Queries are near perfect, also quality of life improvements

This commit is contained in:
austinkelsay 2024-08-05 17:27:19 -05:00
parent e2181722e3
commit e265d1ea88
18 changed files with 169 additions and 262 deletions

45
package-lock.json generated
View File

@ -30,6 +30,7 @@
"zapthreads": "^0.5.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
@ -1275,6 +1276,36 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.51.21",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz",
@ -7249,6 +7280,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@ -31,6 +31,7 @@
"zapthreads": "^0.5.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",

View File

@ -5,70 +5,71 @@ import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay";
import { useCoursesZapsQuery } from "@/hooks/nostrQueries/zaps/useCoursesZapsQuery";
import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery";
const CourseTemplate = ({ course }) => {
const [zapAmount, setZapAmount] = useState(0);
const router = useRouter();
const { returnImageProxy } = useImageProxy();
const { zaps, zapsLoading, zapsError } = useCoursesZapsQuery({ event: course });
const [zapAmount, setZapAmount] = useState(null);
const router = useRouter();
const { returnImageProxy } = useImageProxy();
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: course, type: "course" });
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
let total = 0;
zaps.forEach((zap) => {
// If the zap matches the event or the parameterized event, then add the zap to the total
if (zap.tags.find(tag => tag[0] === "e" && tag[1] === course.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${course.kind}:${course.id}:${course.d}`)) {
const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11");
const invoice = bolt11Tag ? bolt11Tag[1] : null;
if (invoice) {
const amount = getSatAmountFromInvoice(invoice);
total += amount;
}
}
});
setZapAmount(total);
}, [course, zaps, zapsLoading, zapsError]);
let total = 0;
zaps.forEach((zap) => {
// If the zap matches the event or the parameterized event, then add the zap to the total
if (zap.tags.find(tag => tag[0] === "e" && tag[1] === course.id) || zap.tags.find(tag => tag[0] === "a" && tag[1] === `${course.kind}:${course.id}:${course.d}`)) {
const bolt11Tag = zap.tags.find(tag => tag[0] === "bolt11");
const invoice = bolt11Tag ? bolt11Tag[1] : null;
if (invoice) {
const amount = getSatAmountFromInvoice(invoice);
total += amount;
}
}
});
setZapAmount(total);
}, [course, zaps, zapsLoading, zapsError]);
if (zapsError) return <div>Error: {zapsError}</div>;
if (zapsError) return <div>Error: {zapsError}</div>;
return (
<div
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
>
<div
onClick={() => router.push(`/course/${course.id}`)}
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
style={{ paddingBottom: "56.25%" }}
>
<Image
alt="course thumbnail"
src={returnImageProxy(course.image)}
quality={100}
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
<div className="flex flex-col justify-start w-full mt-4">
<h4 className="mb-1 font-bold text-lg font-blinker line-clamp-2">
{course.name || course.title}
</h4>
<p className="text-sm text-gray-500 line-clamp-2">{course.description || course.summary}</p>
<div className="flex flex-row justify-between items-center mt-2">
<p className="text-xs text-gray-400">
{course?.published_at && course.published_at !== "" ? (
formatTimestampToHowLongAgo(course.published_at)
) : (
formatTimestampToHowLongAgo(course.created_at)
)}
</p>
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading} />
</div>
</div>
return (
<div
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
>
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
<div
onClick={() => router.push(`/course/${course.id}`)}
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
style={{ paddingBottom: "56.25%" }}
>
<Image
alt="course thumbnail"
src={returnImageProxy(course.image)}
quality={100}
layout="fill"
objectFit="cover"
className="rounded-md"
/>
</div>
<div className="flex flex-col justify-start w-full mt-4">
<h4 className="mb-1 font-bold text-lg font-blinker line-clamp-2">
{course.name || course.title}
</h4>
<p className="text-sm text-gray-500 line-clamp-2">{course.description || course.summary}</p>
<div className="flex flex-row justify-between items-center mt-2">
<p className="text-xs text-gray-400">
{course?.published_at && course.published_at !== "" ? (
formatTimestampToHowLongAgo(course.published_at)
) : (
formatTimestampToHowLongAgo(course.created_at)
)}
</p>
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading} />
</div>
);
</div>
</div>
);
};
export default CourseTemplate;

View File

@ -3,13 +3,13 @@ import Image from "next/image";
import { useRouter } from "next/router";
import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useResourceZapsQuery } from "@/hooks/nostrQueries/zaps/useResourceZapsQuery";
import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery";
import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay";
const ResourceTemplate = ({ resource }) => {
const [zapAmount, setZapAmount] = useState(0);
const { zaps, zapsLoading, zapsError } = useResourceZapsQuery({ event: resource });
const [zapAmount, setZapAmount] = useState(null);
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: resource, type: "resource" });
const router = useRouter();
const { returnImageProxy } = useImageProxy();

View File

@ -3,18 +3,18 @@ import Image from "next/image";
import { useRouter } from "next/router";
import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useWorkshopsZapsQuery } from "@/hooks/nostrQueries/zaps/useWorkshopsZapsQuery";
import { useZapsQuery } from "@/hooks/nostrQueries/zaps/useZapsQuery";
import { getSatAmountFromInvoice } from "@/utils/lightning";
import ZapDisplay from "@/components/zaps/ZapDisplay";
const WorkshopTemplate = ({ workshop }) => {
const [zapAmount, setZapAmount] = useState(0);
const [zapAmount, setZapAmount] = useState(null);
const router = useRouter();
const { returnImageProxy } = useImageProxy();
const { zaps, zapsLoading, zapsError } = useWorkshopsZapsQuery({ event: workshop });
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: workshop, type: "workshop" });
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
if (zapsLoading || !zaps) return;
let total = 0;
zaps.forEach((zap) => {
@ -28,13 +28,15 @@ const WorkshopTemplate = ({ workshop }) => {
}
}
});
setZapAmount(total);
}, [zaps, workshop, zapsLoading, zapsError]);
if (zapsError) return <div>Error: {zapsError}</div>;
return (
<div className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md">
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
<div
onClick={() => router.push(`/details/${workshop.id}`)}
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"

View File

@ -41,12 +41,12 @@ const WorkshopForm = ({ draft = null }) => {
// Check if it's a YouTube video
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
embedCode = `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allowfullscreen></iframe>`;
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
}
// Check if it's a Vimeo video
else if (videoUrl.includes('vimeo.com')) {
const videoId = videoUrl.split('/').pop();
embedCode = `<iframe width="560" height="315" src="https://player.vimeo.com/video/${videoId}" frameborder="0" allowfullscreen></iframe>`;
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://player.vimeo.com/video/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
}
// Add more conditions here for other video services

View File

@ -12,7 +12,7 @@ const ZapDisplay = ({ zapAmount, event, zapsLoading }) => {
{zapsLoading ? (
<ProgressSpinner style={{width: '20px', height: '20px'}} strokeWidth="8" animationDuration=".5s" />
) : (
zapAmount
zapAmount !== null ? zapAmount : <ProgressSpinner style={{width: '20px', height: '20px'}} strokeWidth="8" animationDuration=".5s" />
)}
</span>
<OverlayPanel className='w-[40%] h-[40%]' ref={op}>

View File

@ -2,6 +2,7 @@ import React from "react";
import { nip19 } from "nostr-tools";
const ZapForm = ({ event }) => {
console.log('event', event);
const nAddress = nip19.naddrEncode({
kind: event?.kind,
pubkey: event?.pubkey,

View File

@ -38,7 +38,7 @@ const { data: courses, isLoading: coursesLoading, error: coursesError, refetch:
queryKey: ['courses', isClient],
queryFn: fetchCoursesFromNDK,
staleTime: 1000 * 60 * 30, // 30 minutes
cacheTime: 1000 * 60 * 60, // 1 hour
refetchInterval: 1000 * 60 * 30, // 30 minutes
enabled: isClient,
})

View File

@ -43,7 +43,7 @@ const { data: resources, isLoading: resourcesLoading, error: resourcesError, ref
queryKey: ['resources', isClient],
queryFn: fetchResourcesFromNDK,
staleTime: 1000 * 60 * 30, // 30 minutes
cacheTime: 1000 * 60 * 60, // 1 hour
refetchInterval: 1000 * 60 * 30, // 30 minutes
enabled: isClient,
})

View File

@ -43,7 +43,7 @@ const { data: workshops, isLoading: workshopsLoading, error: workshopsError, ref
queryKey: ['workshops', isClient],
queryFn: fetchWorkshopsFromNDK,
staleTime: 1000 * 60 * 30, // 30 minutes
cacheTime: 1000 * 60 * 60, // 1 hour
refetchInterval: 1000 * 60 * 30, // 30 minutes
enabled: isClient,
})

View File

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import { useNDKContext } from '@/context/NDKContext';
export function useCoursesZapsQuery({ 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

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import { useNDKContext } from '@/context/NDKContext';
export function useResourceZapsQuery({ 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

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import { useNDKContext } from '@/context/NDKContext';
export function useWorkshopsZapsQuery({ 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

@ -0,0 +1,39 @@
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useNDKContext } from '@/context/NDKContext';
export function useZapsQuery({ event, type }) {
const [isClient, setIsClient] = useState(false);
const ndk = useNDKContext();
useEffect(() => {
setIsClient(true);
}, []);
const fetchZaps = async (event) => {
try {
await ndk.connect();
const filters = [
{ kinds: [9735], "#e": [event.id] },
{ kinds: [9735], "#a": [`${event.kind}:${event.id}:${event.d}`] }
];
const events = await ndk.fetchEvents(filters, { closeOnEose: true });
return events;
} catch (error) {
console.error('Error fetching zaps from NDK:', error);
return [];
}
};
const { data: zaps, isLoading: zapsLoading, error: zapsError, refetch: refetchZaps } = useQuery({
queryKey: ['zaps', isClient, event, type],
queryFn: () => fetchZaps(event),
staleTime: 10000, // 10 seconds
refetchInterval: 10000, // 10 seconds
enabled: isClient,
})
return { zaps, zapsLoading, zapsError, refetchZaps }
}

View File

@ -34,6 +34,7 @@ export function useZapsSubscription({ event }) {
if (isSubscribed) {
uniqueEvents.add(zap);
setZaps(Array.from(uniqueEvents));
setZapsLoading(false);
}
});

View File

@ -38,7 +38,7 @@ export default function Details() {
const ndk = useNDKContext();
const [user] = useLocalStorageWithEffect('user', {});
const { returnImageProxy } = useImageProxy();
const { zaps, zapsError } = useZapsSubscription({ event: processedEvent });
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: processedEvent });
const router = useRouter();
@ -105,8 +105,8 @@ export default function Details() {
useEffect(() => {
if (event) {
const { id, pubkey, content, title, summary, image, published_at, d, topics } = parseEvent(event);
setProcessedEvent({ id, pubkey, content, title, summary, image, published_at, d, topics });
const parsedEvent = parseEvent(event);
setProcessedEvent(parsedEvent);
}
}, [event]);
@ -184,7 +184,7 @@ export default function Details() {
</div>
) : (
<div className="w-full flex justify-end">
<ZapDisplay zapAmount={zapAmount} event={processedEvent} />
<ZapDisplay zapAmount={zapAmount} event={processedEvent} zapsLoading={zapsLoading} />
</div>
)}
</div>

View File

@ -29,5 +29,8 @@ module.exports = {
},
},
},
plugins: [],
plugins: [
require('@tailwindcss/typography'),
// ...
],
}