Fix loading states for carousels and zaps, reorganized nostrQueries, added zap subscription hook for details

This commit is contained in:
austinkelsay 2024-08-05 15:25:04 -05:00
parent c5eb71e4b8
commit e2181722e3
15 changed files with 102 additions and 77 deletions

View File

@ -3,7 +3,7 @@ import { Carousel } from 'primereact/carousel';
import { parseCourseEvent } from '@/utils/nostr'; import { parseCourseEvent } from '@/utils/nostr';
import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate'; import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
import { useCoursesQuery } from '@/hooks/nostrQueries/useCoursesQuery'; import { useCoursesQuery } from '@/hooks/nostrQueries/content/useCoursesQuery';
const responsiveOptions = [ const responsiveOptions = [
{ {
@ -48,21 +48,17 @@ export default function CoursesCarousel() {
return <div>Error: {coursesError.message}</div> return <div>Error: {coursesError.message}</div>
} }
if (coursesLoading) {
return <div>Loading...</div>
}
return ( return (
<> <>
<h2 className="ml-[6%] mt-4">Courses</h2> <h2 className="ml-[6%] mt-4">Courses</h2>
<div className={"min-h-[384px]"}> <div className={"min-h-[384px]"}>
<Carousel <Carousel
value={!processedCourses.length > 0 ? [{}, {}, {}] : [...processedCourses]} value={coursesLoading || !processedCourses.length ? [{}, {}, {}] : [...processedCourses]}
numVisible={2} numVisible={2}
itemTemplate={(item) => itemTemplate={(item) =>
processedCourses.length > 0 ? !processedCourses.length ?
<CourseTemplate key={item.id} course={item} /> : <TemplateSkeleton key={Math.random()} /> :
<TemplateSkeleton key={Math.random()} /> <CourseTemplate key={item.id} course={item} />
} }
responsiveOptions={responsiveOptions} /> responsiveOptions={responsiveOptions} />
</div> </div>

View File

@ -3,7 +3,7 @@ import { Carousel } from 'primereact/carousel';
import { parseEvent } from '@/utils/nostr'; import { parseEvent } from '@/utils/nostr';
import ResourceTemplate from '@/components/content/carousels/templates/ResourceTemplate'; import ResourceTemplate from '@/components/content/carousels/templates/ResourceTemplate';
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
import { useResourcesQuery } from '@/hooks/nostrQueries/useResourcesQuery'; import { useResourcesQuery } from '@/hooks/nostrQueries/content/useResourcesQuery';
const responsiveOptions = [ const responsiveOptions = [
{ {
@ -42,10 +42,6 @@ export default function ResourcesCarousel() {
fetch(); fetch();
}, [resources]); }, [resources]);
if (resourcesLoading) {
return <div>Loading...</div>
}
if (resourcesError) { if (resourcesError) {
return <div>Error: {resourcesError.message}</div> return <div>Error: {resourcesError.message}</div>
} }
@ -53,7 +49,8 @@ export default function ResourcesCarousel() {
return ( return (
<> <>
<h2 className="ml-[6%] mt-4">Resources</h2> <h2 className="ml-[6%] mt-4">Resources</h2>
<Carousel value={!processedResources.length > 0 ? [{}, {}, {}] : [...processedResources]} <Carousel
value={resourcesLoading || !processedResources.length ? [{}, {}, {}] : [...processedResources]}
numVisible={2} numVisible={2}
itemTemplate={(item) => itemTemplate={(item) =>
processedResources.length > 0 ? processedResources.length > 0 ?

View File

@ -3,7 +3,7 @@ import { Carousel } from 'primereact/carousel';
import { parseEvent } from '@/utils/nostr'; import { parseEvent } from '@/utils/nostr';
import WorkshopTemplate from '@/components/content/carousels/templates/WorkshopTemplate'; import WorkshopTemplate from '@/components/content/carousels/templates/WorkshopTemplate';
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton'; import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
import { useWorkshopsQuery } from '@/hooks/nostrQueries/useWorkshopsQuery'; import { useWorkshopsQuery } from '@/hooks/nostrQueries/content/useWorkshopsQuery';
const responsiveOptions = [ const responsiveOptions = [
{ {
@ -24,15 +24,14 @@ const responsiveOptions = [
]; ];
export default function WorkshopsCarousel() { export default function WorkshopsCarousel() {
const [processedWorkshops, setProcessedWorkshops] = useState([]) const [processedWorkshops, setProcessedWorkshops] = useState([]);
const { workshops, workshopsLoading, workshopsError, refetchWorkshops } = useWorkshopsQuery() const { workshops, workshopsLoading, workshopsError, refetchWorkshops } = useWorkshopsQuery();
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
try { try {
if (workshops && workshops.length > 0) { if (workshops && workshops.length > 0) {
const processedWorkshops = workshops.map(workshop => parseEvent(workshop)); const processedWorkshops = workshops.map(workshop => parseEvent(workshop));
setProcessedWorkshops(processedWorkshops); setProcessedWorkshops(processedWorkshops);
} else { } else {
console.log('No workshops fetched or empty array returned'); console.log('No workshops fetched or empty array returned');
@ -44,20 +43,21 @@ export default function WorkshopsCarousel() {
fetch(); fetch();
}, [workshops]); }, [workshops]);
if (workshopsLoading) return <div>Loading...</div>;
if (workshopsError) return <div>Error: {workshopsError}</div>; if (workshopsError) return <div>Error: {workshopsError}</div>;
return ( return (
<> <>
<h2 className="ml-[6%] mt-4">Workshops</h2> <h2 className="ml-[6%] mt-4">Workshops</h2>
<Carousel value={!processedWorkshops.length > 0 ? [{}, {}, {}] : [...processedWorkshops]} <Carousel
value={workshopsLoading || !processedWorkshops.length ? [{}, {}, {}] : [...processedWorkshops]}
numVisible={2} numVisible={2}
itemTemplate={(item) => itemTemplate={(item) =>
processedWorkshops.length > 0 ? !processedWorkshops.length ?
<WorkshopTemplate key={item.id} workshop={item} /> : <TemplateSkeleton key={Math.random()} /> :
<TemplateSkeleton key={Math.random()} /> <WorkshopTemplate key={item.id} workshop={item} />
} }
responsiveOptions={responsiveOptions} /> responsiveOptions={responsiveOptions}
/>
</> </>
); );
} }

View File

@ -5,7 +5,7 @@ import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { getSatAmountFromInvoice } from "@/utils/lightning"; import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay"; import ZapDisplay from "@/components/zaps/ZapDisplay";
import { useCoursesZapsQuery } from "@/hooks/nostrQueries/useCoursesZapsQuery"; import { useCoursesZapsQuery } from "@/hooks/nostrQueries/zaps/useCoursesZapsQuery";
const CourseTemplate = ({ course }) => { const CourseTemplate = ({ course }) => {
const [zapAmount, setZapAmount] = useState(0); const [zapAmount, setZapAmount] = useState(0);
@ -31,7 +31,6 @@ const CourseTemplate = ({ course }) => {
setZapAmount(total); setZapAmount(total);
}, [course, zaps, zapsLoading, zapsError]); }, [course, zaps, zapsLoading, zapsError]);
if (zapsLoading) return <div>Loading...</div>;
if (zapsError) return <div>Error: {zapsError}</div>; if (zapsError) return <div>Error: {zapsError}</div>;
return ( return (
@ -65,7 +64,7 @@ const CourseTemplate = ({ course }) => {
formatTimestampToHowLongAgo(course.created_at) formatTimestampToHowLongAgo(course.created_at)
)} )}
</p> </p>
<ZapDisplay zapAmount={zapAmount} event={course} /> <ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { formatTimestampToHowLongAgo } from "@/utils/time"; import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { useResourceZapsQuery } from "@/hooks/nostrQueries/useResourceZapsQuery"; import { useResourceZapsQuery } from "@/hooks/nostrQueries/zaps/useResourceZapsQuery";
import { getSatAmountFromInvoice } from "@/utils/lightning"; import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay"; import ZapDisplay from "@/components/zaps/ZapDisplay";
@ -32,7 +32,6 @@ const ResourceTemplate = ({ resource }) => {
setZapAmount(total); setZapAmount(total);
}, [resource, zaps, zapsLoading, zapsError]); }, [resource, zaps, zapsLoading, zapsError]);
if (zapsLoading) return <div>Loading...</div>;
if (zapsError) return <div>Error: {zapsError}</div>; if (zapsError) return <div>Error: {zapsError}</div>;
return ( return (
@ -63,7 +62,7 @@ const ResourceTemplate = ({ resource }) => {
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
{formatTimestampToHowLongAgo(resource.published_at)} {formatTimestampToHowLongAgo(resource.published_at)}
</p> </p>
<ZapDisplay zapAmount={zapAmount} event={resource} /> <ZapDisplay zapAmount={zapAmount} event={resource} zapsLoading={zapsLoading} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { formatTimestampToHowLongAgo } from "@/utils/time"; import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { useWorkshopsZapsQuery } from "@/hooks/nostrQueries/useWorkshopsZapsQuery"; import { useWorkshopsZapsQuery } from "@/hooks/nostrQueries/zaps/useWorkshopsZapsQuery";
import { getSatAmountFromInvoice } from "@/utils/lightning"; import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay"; import ZapDisplay from "@/components/zaps/ZapDisplay";
@ -31,7 +31,6 @@ const WorkshopTemplate = ({ workshop }) => {
setZapAmount(total); setZapAmount(total);
}, [zaps, workshop, zapsLoading, zapsError]); }, [zaps, workshop, zapsLoading, zapsError]);
if (zapsLoading) return <div>Loading...</div>;
if (zapsError) return <div>Error: {zapsError}</div>; if (zapsError) return <div>Error: {zapsError}</div>;
return ( return (
@ -59,7 +58,7 @@ const WorkshopTemplate = ({ workshop }) => {
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
{formatTimestampToHowLongAgo(workshop.published_at)} {formatTimestampToHowLongAgo(workshop.published_at)}
</p> </p>
<ZapDisplay zapAmount={zapAmount} event={workshop} /> <ZapDisplay zapAmount={zapAmount} event={workshop} zapsLoading={zapsLoading} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,16 +3,16 @@ import { OverlayPanel } from 'primereact/overlaypanel';
import ZapForm from './ZapForm'; import ZapForm from './ZapForm';
import { ProgressSpinner } from 'primereact/progressspinner'; import { ProgressSpinner } from 'primereact/progressspinner';
const ZapDisplay = ({ zapAmount, event }) => { const ZapDisplay = ({ zapAmount, event, zapsLoading }) => {
const op = useRef(null); const op = useRef(null);
return ( return (
<> <>
<span className="text-xs cursor-pointer" onClick={(e) => op.current.toggle(e)}> <span className="text-xs cursor-pointer flex items-center" onClick={(e) => op.current.toggle(e)}>
<i className="pi pi-bolt text-yellow-300"></i> <i className="pi pi-bolt text-yellow-300"></i>
{zapAmount || zapAmount === 0 ? ( {zapsLoading ? (
zapAmount <ProgressSpinner style={{width: '20px', height: '20px'}} strokeWidth="8" animationDuration=".5s" />
) : ( ) : (
<ProgressSpinner style={{ display: 'inline-block' }} /> zapAmount
)} )}
</span> </span>
<OverlayPanel className='w-[40%] h-[40%]' ref={op}> <OverlayPanel className='w-[40%] h-[40%]' ref={op}>

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import { useNDKContext } from '@/context/NDKContext';
export function useZapsSubscription({ event }) {
const [isClient, setIsClient] = useState(false);
const [zaps, setZaps] = useState([]);
const [zapsLoading, setZapsLoading] = useState(true);
const [zapsError, setZapsError] = useState(null);
const ndk = useNDKContext();
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
if (!isClient || !ndk || !event) return;
let subscription = null;
let isSubscribed = true;
const fetchZapsFromNDK = async () => {
try {
await ndk.connect();
const uniqueEvents = new Set();
const filters = [
{ kinds: [9735], "#e": [event.id] },
{ kinds: [9735], "#a": [`${event.kind}:${event.id}:${event.d}`] }
];
subscription = ndk.subscribe(filters);
subscription.on('event', (zap) => {
if (isSubscribed) {
uniqueEvents.add(zap);
setZaps(Array.from(uniqueEvents));
}
});
subscription.on('eose', () => {
setZaps(Array.from(uniqueEvents));
setZapsLoading(false);
});
} catch (error) {
setZapsError('Error fetching zaps from NDK: ' + error);
setZapsLoading(false);
}
};
fetchZapsFromNDK();
return () => {
isSubscribed = false;
if (subscription) {
subscription.stop();
}
};
}, [isClient, ndk, event]);
return { zaps, zapsLoading, zapsError };
}

View File

@ -11,6 +11,7 @@ import Image from 'next/image';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import { useNDKContext } from '@/context/NDKContext'; import { useNDKContext } from '@/context/NDKContext';
import { useZapsSubscription } from '@/hooks/nostrQueries/zaps/useZapsSubscription';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
const MDDisplay = dynamic( const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"), () => import("@uiw/react-markdown-preview"),
@ -32,12 +33,12 @@ export default function Details() {
const [author, setAuthor] = useState(null); const [author, setAuthor] = useState(null);
const [bitcoinConnect, setBitcoinConnect] = useState(false); const [bitcoinConnect, setBitcoinConnect] = useState(false);
const [nAddress, setNAddress] = useState(null); const [nAddress, setNAddress] = useState(null);
const [zaps, setZaps] = useState([]);
const [zapAmount, setZapAmount] = useState(0); const [zapAmount, setZapAmount] = useState(0);
const ndk = useNDKContext(); const ndk = useNDKContext();
const [user] = useLocalStorageWithEffect('user', {}); const [user] = useLocalStorageWithEffect('user', {});
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { zaps, zapsError } = useZapsSubscription({ event: processedEvent });
const router = useRouter(); const router = useRouter();
@ -135,34 +136,6 @@ export default function Details() {
setZapAmount(total); setZapAmount(total);
}, [zaps]); }, [zaps]);
useEffect(() => {
const fetchZaps = async () => {
try {
const processed = parseEvent(event);
await ndk.connect();
const filters = [
{
kinds: [9735],
"#e": [processed.id]
},
{
kinds: [9734],
"#a": [`${processed.kind}:${processed.id}:${processed.d}`]
}
]
const zaps = await ndk.fetchEvents(filters);
setZaps(zaps);
} catch (error) {
console.error('Error fetching zaps:', error);
}
}
if (event && ndk) {
fetchZaps();
}
}, [ndk, event]);
return ( return (
<div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'> <div className='w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2'>
<div className='w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'> <div className='w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col'>