diff --git a/src/components/content/carousels/CoursesCarousel.js b/src/components/content/carousels/CoursesCarousel.js
index e957f74..056eadb 100644
--- a/src/components/content/carousels/CoursesCarousel.js
+++ b/src/components/content/carousels/CoursesCarousel.js
@@ -1,7 +1,5 @@
-import React, { useState, useEffect, use } from 'react';
+import React, { useState, useEffect } from 'react';
import { Carousel } from 'primereact/carousel';
-import { useRouter } from 'next/router';
-import { useImageProxy } from '@/hooks/useImageProxy';
import { parseEvent } from '@/utils/nostr';
import { useNostr } from '@/hooks/useNostr';
import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
@@ -27,11 +25,8 @@ const responsiveOptions = [
export default function CoursesCarousel() {
const [processedCourses, setProcessedCourses] = useState([]);
- const [screenWidth, setScreenWidth] = useState(null);
const [courses, setCourses] = useState([]);
- const router = useRouter();
const { fetchCourses, events } = useNostr();
- const { returnImageProxy } = useImageProxy();
useEffect(() => {
if (events && events.courses && events.courses.length > 0) {
@@ -41,35 +36,6 @@ export default function CoursesCarousel() {
}
}, [events]);
- useEffect(() => {
- // Update the state to the current window width
- setScreenWidth(window.innerWidth);
-
- const handleResize = () => {
- // Update the state to the new window width when it changes
- setScreenWidth(window.innerWidth);
- };
-
- window.addEventListener('resize', handleResize);
-
- // Remove the event listener on cleanup
- return () => window.removeEventListener('resize', handleResize);
- }, []); // The empty array ensures this effect only runs once, similar to componentDidMount
-
-
- const calculateImageDimensions = () => {
- if (screenWidth >= 1200) {
- // Large screens
- return { width: 426, height: 240 };
- } else if (screenWidth >= 768 && screenWidth < 1200) {
- // Medium screens
- return { width: 344, height: 194 };
- } else {
- // Small screens
- return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) };
- }
- };
-
useEffect(() => {
const processCourses = courses.map(course => {
const { id, content, title, summary, image, published_at } = parseEvent(course);
diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js
index 6fff862..3c92cd1 100644
--- a/src/components/content/carousels/templates/CourseTemplate.js
+++ b/src/components/content/carousels/templates/CourseTemplate.js
@@ -1,14 +1,27 @@
-import React from "react";
+import React, {useEffect, useState} from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import useResponsiveImageDimensions from "@/hooks/useResponsiveImageDimensions";
import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
+import { useNostr } from "@/hooks/useNostr";
const CourseTemplate = (course) => {
+ const [zaps, setZaps] = useState([]);
const router = useRouter();
const { returnImageProxy } = useImageProxy();
const { width, height } = useResponsiveImageDimensions();
+ const {events, fetchZapsForEvent} = useNostr();
+
+ useEffect(() => {
+ if (events && events.zaps) {
+ console.log('zaps:', events.zaps);
+ setZaps(events.zaps);
+ } else {
+ fetchZapsForEvent(course.id);
+ }
+ }, [events]);
+
return (
router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js
index 7d6280e..05ac10a 100644
--- a/src/hooks/useNostr.js
+++ b/src/hooks/useNostr.js
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
-import { SimplePool, nip19, verifyEvent } from "nostr-tools";
+import { SimplePool, nip19, verifyEvent, nip57 } from "nostr-tools";
import axios from "axios";
import { useToast } from "./useToast";
@@ -20,7 +20,8 @@ export const useNostr = () => {
resources: [],
workshops: [],
courses: [],
- streams: []
+ streams: [],
+ zaps: []
});
const { showToast } = useToast();
@@ -53,6 +54,9 @@ export const useNostr = () => {
try {
const sub = pool.current.subscribeMany(relays, filter, {
onevent: async (event) => {
+ if (event.kind === 9735) {
+ console.log('event:', event);
+ }
const shouldInclude = await hasRequiredTags(event.tags);
if (shouldInclude) {
setEvents(prevData => ({
@@ -75,6 +79,84 @@ export const useNostr = () => {
}
};
+ // zaps
+ // 1. get the author from the content
+ // 2. get the author's kind0
+ // 3. get the author's lud16 if available
+ // 4. Make a get request to the lud16 endpoint and ensure that allowNostr is true
+ // 5. Create zap request event and sign it
+ // 6. Send to the callback url as a get req with the nostr event as a query param
+ // 7. get the invoice back and pay it with webln
+ // 8. listen for the zap receipt event and update the UI
+
+ const zapEvent = async (event) => {
+ const kind0 = await fetchKind0([{ authors: [event.pubkey], kinds: [0] }], {});
+
+ if (Object.keys(kind0).length === 0) {
+ console.error('Error fetching kind0');
+ return;
+ }
+
+ if (kind0?.lud16) {
+ const lud16Username = kind0.lud16.split('@')[0];
+ const lud16Domain = kind0.lud16.split('@')[1];
+
+ const lud16Url = `https://${lud16Domain}/.well-known/lnurlp/${lud16Username}`;
+
+ const response = await axios.get(lud16Url);
+
+ if (response.data.allowsNostr) {
+ const zapReq = nip57.makeZapRequest({
+ profile: event.pubkey,
+ event: event.id,
+ amount: 1000,
+ relays: relays,
+ comment: 'Plebdevs Zap'
+ });
+
+ console.log('zapReq:', zapReq);
+
+ const signedEvent = await window?.nostr.signEvent(zapReq);
+
+ const callbackUrl = response.data.callback;
+
+ const zapRequestAPICall = `${callbackUrl}?amount=${1000}&nostr=${encodeURI(JSON.stringify(signedEvent))}`;
+
+ const invoiceResponse = await axios.get(zapRequestAPICall);
+
+ if (invoiceResponse?.data?.pr) {
+ const invoice = invoiceResponse.data.pr;
+
+ const enabled = await window?.webln?.enable();
+
+ console.log('webln enabled:', enabled);
+
+ const payInvoiceResponse = await window?.webln?.sendPayment(invoice);
+
+ console.log('payInvoiceResponse:', payInvoiceResponse);
+ } else {
+ console.error('Error fetching invoice');
+ showToast('error', 'Error', 'Error fetching invoice');
+ }
+ }
+ } else if (kind0?.lud06) {
+ // handle lnurlpay
+ } else {
+ showToast('error', 'Error', 'User has no Lightning Address or LNURL');
+ return;
+ }
+
+ }
+
+ const fetchZapsForEvent = async (eventId) => {
+ const filter = [{ kinds: [9735] }];
+ const hasRequiredTags = async (eventData) => {
+ const hasEtag = eventData.some(([tag, value]) => tag === "e" && value === eventId);
+ return hasEtag;
+ };
+ fetchEvents(filter, 'zaps', hasRequiredTags);
+ }
+
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
@@ -288,6 +370,8 @@ export const useNostr = () => {
fetchCourses,
fetchWorkshops,
// fetchStreams,
+ zapEvent,
+ fetchZapsForEvent,
getRelayStatuses,
events
};
diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js
index 26e2a33..aa31f7b 100644
--- a/src/pages/details/[slug].js
+++ b/src/pages/details/[slug].js
@@ -27,10 +27,18 @@ export default function Details() {
const [author, setAuthor] = useState(null);
const { returnImageProxy } = useImageProxy();
- const { fetchSingleEvent, fetchKind0 } = useNostr();
+ const { fetchSingleEvent, fetchKind0, zapEvent } = useNostr();
const router = useRouter();
+ const handleZapEvent = async () => {
+ if (!event) return;
+
+ const response = await zapEvent(event);
+
+ console.log('zap response:', response);
+ }
+
useEffect(() => {
if (router.isReady) {
const { slug } = router.query;
@@ -112,6 +120,7 @@ export default function Details() {
label="Zap"
severity="success"
outlined
+ onClick={handleZapEvent}
pt={{
button: {
icon: ({ context }) => ({