Improvements to progress spinners, fixed resource and course payment buttons to not have to fetch invoice until button is pressed

This commit is contained in:
austinkelsay 2024-09-17 13:28:58 -05:00
parent ff3e907677
commit a0a9b9fcc8
21 changed files with 167 additions and 117 deletions

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
}
}

View File

@ -1,67 +1,78 @@
import React, { useEffect, useState } from 'react';
import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog'; // Import Dialog component
import { initializeBitcoinConnect } from './BitcoinConnect';
import { Dialog } from 'primereact/dialog';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { ProgressSpinner } from 'primereact/progressspinner';
import axios from 'axios';
import GenericButton from '@/components/buttons/GenericButton';
import axios from 'axios'; // Import axios for API calls
import { useRouter } from 'next/router';
const Payment = dynamic(
() => import('@getalby/bitcoin-connect-react').then((mod) => mod.Payment),
{
ssr: false,
}
{ ssr: false }
);
const CoursePaymentButton = ({ lnAddress, amount, onSuccess, onError, courseId }) => {
const [invoice, setInvoice] = useState(null);
const [userId, setUserId] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const { showToast } = useToast();
const { data: session, status } = useSession();
const [dialogVisible, setDialogVisible] = useState(false); // New state for dialog visibility
const [dialogVisible, setDialogVisible] = useState(false);
const router = useRouter();
useEffect(() => {
initializeBitcoinConnect();
}, []);
useEffect(() => {
if (session && session.user) {
setUserId(session.user.id);
let intervalId;
if (invoice) {
intervalId = setInterval(async () => {
const paid = await invoice.verifyPayment();
if (paid && invoice.preimage) {
clearInterval(intervalId);
// handle success
handlePaymentSuccess({ paid, preimage: invoice.preimage });
}
}, 2000);
} else {
console.log('no invoice');
}
}, [status, session]);
useEffect(() => {
const fetchInvoice = async () => {
try {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const invoice = await ln.requestInvoice({ satoshi: amount });
setInvoice(invoice);
} catch (error) {
console.error('Error fetching invoice:', error);
showToast('error', 'Invoice Error', 'Failed to fetch the invoice.');
if (onError) onError(error);
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [invoice]);
fetchInvoice();
}, [lnAddress, amount, onError, showToast]);
const fetchInvoice = async () => {
setIsLoading(true);
try {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const invoice = await ln.requestInvoice({ satoshi: amount });
setInvoice(invoice);
setDialogVisible(true);
} catch (error) {
console.error('Error fetching invoice:', error);
showToast('error', 'Invoice Error', 'Failed to fetch the invoice.');
if (onError) onError(error);
}
setIsLoading(false);
};
useEffect(() => {
console.log('invoice', invoice);
}, [invoice]);
const handlePaymentSuccess = async (response) => {
try {
const purchaseData = {
userId: userId,
userId: session.user.id,
courseId: courseId,
amountPaid: parseInt(amount, 10)
};
console.log('purchaseData', purchaseData);
const result = await axios.post('/api/purchase/course', purchaseData);
if (result.status === 200) {
@ -75,27 +86,36 @@ const CoursePaymentButton = ({ lnAddress, amount, onSuccess, onError, courseId }
showToast('error', 'Purchase Update Failed', 'Payment was successful, but failed to update user purchases.');
if (onError) onError(error);
}
setDialogVisible(false); // Close the dialog on successful payment
setDialogVisible(false);
};
return (
<>
<GenericButton
label={`${amount} sats`}
icon="pi pi-wallet"
onClick={() => {
if (status === 'unauthenticated') {
console.log('unauthenticated');
router.push('/auth/signin');
} else {
setDialogVisible(true);
fetchInvoice();
}
}}
disabled={!invoice}
disabled={isLoading}
severity='primary'
rounded
icon='pi pi-wallet'
className='text-[#f8f8ff] text-sm'
className={`text-[#f8f8ff] text-sm ${isLoading ? 'hidden' : ''}`}
/>
{isLoading && (
<div className='w-full h-full flex items-center justify-center'>
<ProgressSpinner
style={{ width: '30px', height: '30px' }}
strokeWidth="8"
animationDuration=".5s"
/>
</div>
)}
<Dialog
visible={dialogVisible}
onHide={() => setDialogVisible(false)}

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { Dialog } from 'primereact/dialog';
import { initializeBitcoinConnect } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
@ -17,43 +16,56 @@ const Payment = dynamic(
const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resourceId }) => {
const [invoice, setInvoice] = useState(null);
const [userId, setUserId] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const { showToast } = useToast();
const { data: session, status } = useSession();
const [dialogVisible, setDialogVisible] = useState(false);
const router = useRouter();
useEffect(() => {
initializeBitcoinConnect();
}, []);
let intervalId;
if (invoice) {
intervalId = setInterval(async () => {
const paid = await invoice.verifyPayment();
useEffect(() => {
if (session && session.user) {
setUserId(session.user.id);
if (paid && invoice.preimage) {
clearInterval(intervalId);
// handle success
handlePaymentSuccess({ paid, preimage: invoice.preimage });
}
}, 2000);
} else {
console.log('no invoice');
}
}, [status, session]);
useEffect(() => {
const fetchInvoice = async () => {
try {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const invoice = await ln.requestInvoice({ satoshi: amount });
setInvoice(invoice);
} catch (error) {
console.error('Error fetching invoice:', error);
showToast('error', 'Invoice Error', 'Failed to fetch the invoice.');
if (onError) onError(error);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [invoice]);
fetchInvoice();
}, [lnAddress, amount, onError, showToast]);
const fetchInvoice = async () => {
setIsLoading(true);
try {
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const invoice = await ln.requestInvoice({ satoshi: amount });
setInvoice(invoice);
setDialogVisible(true);
} catch (error) {
console.error('Error fetching invoice:', error);
showToast('error', 'Invoice Error', 'Failed to fetch the invoice.');
if (onError) onError(error);
}
setIsLoading(false);
};
const handlePaymentSuccess = async (response) => {
console.log('handlePaymentSuccess', response);
try {
const purchaseData = {
userId: userId,
userId: session.user.id,
resourceId: resourceId,
amountPaid: parseInt(amount, 10)
};
@ -76,31 +88,31 @@ const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resource
return (
<>
{
invoice ? (
<GenericButton
label={`${amount} sats`}
icon="pi pi-wallet"
onClick={() => {
if (status === 'unauthenticated') {
console.log('unauthenticated');
router.push('/auth/signin');
} else {
setDialogVisible(true);
}
}}
disabled={!invoice}
severity='primary'
rounded
className="text-[#f8f8ff] text-sm"
/>
) : (
<GenericButton
label={`${amount} sats`}
icon="pi pi-wallet"
onClick={() => {
if (status === 'unauthenticated') {
console.log('unauthenticated');
router.push('/auth/signin');
} else {
fetchInvoice();
}
}}
disabled={isLoading}
severity='primary'
rounded
className={`text-[#f8f8ff] text-sm ${isLoading ? 'hidden' : ''}`}
/>
{isLoading && (
<div className='w-full h-full flex items-center justify-center'>
<ProgressSpinner
style={{ width: '30px', height: '30px' }}
strokeWidth="8"
animationDuration=".5s"
/>
)}
</div>
)}
<Dialog
visible={dialogVisible}
onHide={() => setDialogVisible(false)}

View File

@ -47,7 +47,7 @@ export function CourseTemplate({ course }) {
}
}, [course]);
if (!nAddress) return <ProgressSpinner />;
if (!nAddress) return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
if (zapsError) return <div>Error: {zapsError}</div>;

View File

@ -116,9 +116,7 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec
if (!processedEvent || !author) {
return (
<div className="flex justify-center items-center h-screen">
<ProgressSpinner />
</div>
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
);
}

View File

@ -16,6 +16,7 @@ import useWindowWidth from "@/hooks/useWindowWidth";
import { useNDKContext } from "@/context/NDKContext";
import { findKind0Fields } from '@/utils/nostr';
import { defaultRelayUrls } from "@/context/NDKContext";
import { ProgressSpinner } from 'primereact/progressspinner';
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
@ -108,7 +109,7 @@ export default function CourseDetailsNew({ processedEvent, paidCourse, lessons,
};
if (!processedEvent || !author) {
return <div>Loading...</div>;
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
}
return (

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import CommunityMessage from "@/components/feeds/messages/CommunityMessage";
import { parseMessageEvent, findKind0Fields } from "@/utils/nostr";
import { ProgressSpinner } from 'primereact/progressspinner';
import { useNDKContext } from "@/context/NDKContext";
const MessageDropdownItem = ({ message, onSelect }) => {
@ -79,7 +80,7 @@ const MessageDropdownItem = ({ message, onSelect }) => {
return (
<div className="w-full border-t-2 border-gray-700 py-4">
{loading ? (
<div>Loading...</div>
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
) : (
<CommunityMessage message={messageWithAuthor ? messageWithAuthor : message} platform={platform} />
)}

View File

@ -1,8 +1,7 @@
import React from 'react';
import React, { useMemo } from 'react';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useDiscordQuery } from '@/hooks/communityQueries/useDiscordQuery';
import { useRouter } from 'next/router';
import { highlightText } from '@/utils/text';
import CommunityMessage from '@/components/feeds/messages/CommunityMessage';
import useWindowWidth from '@/hooks/useWindowWidth';
@ -11,6 +10,14 @@ const DiscordFeed = ({ searchQuery }) => {
const { data, error, isLoading } = useDiscordQuery({page: router.query.page});
const windowWidth = useWindowWidth();
// Memoize the filtered data
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(message =>
message.content.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [data, searchQuery]);
if (isLoading) {
return (
<div className="h-[100vh] min-bottom-bar:w-[86vw] max-sidebar:w-[100vw]">
@ -23,14 +30,10 @@ const DiscordFeed = ({ searchQuery }) => {
return <div className="text-red-500 text-center p-4">Failed to load messages. Please try again later.</div>;
}
const filteredData = data.filter(message =>
message.content.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[86vw]">
<div className="mx-4">
{filteredData && filteredData.length > 0 ? (
{filteredData.length > 0 ? (
filteredData.map(message => (
<CommunityMessage
key={message.id}

View File

@ -165,7 +165,7 @@ const CourseForm = ({ draft = null }) => {
};
if (documentsLoading || videosLoading || draftsLoading) {
return <ProgressSpinner />;
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
}
return (

View File

@ -142,7 +142,7 @@ const UserContent = () => {
<div className="w-full mx-auto my-8">
<div className="w-full mx-auto px-8 max-tab:px-0">
{isLoading ? (
<ProgressSpinner className="w-full mx-auto" />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
) : isError ? (
<p>Error loading content.</p>
) : content.length > 0 ? (

View File

@ -89,7 +89,7 @@ const UserProfile = () => {
)}
</div>
{!session || !session?.user || !ndk ? (
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
) : (
<DataTable
emptyMessage="No purchases"

View File

@ -173,7 +173,7 @@ const UserSettings = () => {
</div>
</div>
{!session || !session?.user || !ndk ? (
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
) : (
<>
<Panel

View File

@ -157,7 +157,7 @@ const SubscribeModal = ({ user }) => {
>
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
<span className="ml-2">Processing subscription...</span>
</div>
) : (

View File

@ -146,7 +146,7 @@ const UserSubscription = ({ user }) => {
<Card title="Subscribe to PlebDevs" className="mb-6">
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
<span className="ml-2">Processing subscription...</span>
</div>
) : (

17
src/config/appConfig.js Normal file
View File

@ -0,0 +1,17 @@
const appConfig = {
defaultRelayUrls: [
"wss://nos.lol/",
"wss://relay.damus.io/",
"wss://relay.snort.social/",
"wss://relay.nostr.band/",
"wss://relay.mutinywallet.com/",
"wss://relay.primal.net/",
"wss://nostr21.com/",
"wss://nostrue.com/",
"wss://purplerelay.com/",
// "wss://relay.devs.tools/"
],
authorPubkeys: ["8cb60e215678879cda0bef4d5b3fc1a5c5925d2adb5d8c4fa7b7d03b5f2deaea"]
};
export default appConfig;

View File

@ -7,6 +7,8 @@ import DraftCourseLesson from "@/components/content/courses/DraftCourseLesson";
import { useNDKContext } from "@/context/NDKContext";
import { useSession } from "next-auth/react";
import { useIsAdmin } from "@/hooks/useIsAdmin";
import { ProgressSpinner } from 'primereact/progressspinner';
const DraftCourse = () => {
const { data: session, status } = useSession();
const [course, setCourse] = useState(null);
@ -94,7 +96,7 @@ const DraftCourse = () => {
}, [lessons, ndk, fetchAuthor, session, status]);
if (status === "loading") {
return <div>Loading...</div>;
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
}
return (

View File

@ -194,9 +194,7 @@ const Course = () => {
if (loading) {
return (
<div className="flex justify-center items-center h-screen">
<ProgressSpinner />
</div>
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
);
}

View File

@ -41,7 +41,7 @@ const Create = () => {
if (!isAdmin) return null;
if (isLoading) return <ProgressSpinner />;
if (isLoading) return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>;
return (
<div className="w-full min-bottom-bar:w-[86vw] max-sidebar:w-[100vw] px-8 mx-auto my-8 flex flex-col justify-center">

View File

@ -192,9 +192,7 @@ export default function Details() {
}
if (loading) {
return <div className="mx-auto">
<ProgressSpinner />
</div>;
return <div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
}
if (error) {

View File

@ -41,7 +41,7 @@ const Profile = () => {
if (status === 'loading' || isLoading) {
return (
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
);
}

View File

@ -170,7 +170,7 @@ const Subscribe = () => {
<Card title="Subscribe to PlebDevs" className="mb-6">
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
<span className="ml-2">Processing subscription...</span>
</div>
) : (