mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
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:
parent
ff3e907677
commit
a0a9b9fcc8
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)}
|
||||
|
@ -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)}
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
|
@ -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 ? (
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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
17
src/config/appConfig.js
Normal 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;
|
@ -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 (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
Loading…
x
Reference in New Issue
Block a user