diff --git a/package-lock.json b/package-lock.json
index 4c9c346..a6b165c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@prisma/client": "^5.9.1",
"@reduxjs/toolkit": "^2.1.0",
"axios": "^1.6.7",
+ "bech32": "^2.0.0",
"classnames": "^2.5.1",
"light-bolt11-decoder": "^3.1.1",
"next": "14.0.4",
@@ -1378,6 +1379,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/bech32": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
+ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
diff --git a/package.json b/package.json
index b71b5df..049b7b5 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@prisma/client": "^5.9.1",
"@reduxjs/toolkit": "^2.1.0",
"axios": "^1.6.7",
+ "bech32": "^2.0.0",
"classnames": "^2.5.1",
"light-bolt11-decoder": "^3.1.1",
"next": "14.0.4",
diff --git a/src/components/content/carousels/templates/CourseTemplate.js b/src/components/content/carousels/templates/CourseTemplate.js
index 1b6fdc6..22a7110 100644
--- a/src/components/content/carousels/templates/CourseTemplate.js
+++ b/src/components/content/carousels/templates/CourseTemplate.js
@@ -5,73 +5,71 @@ import { formatTimestampToHowLongAgo } from "@/utils/time";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useNostr } from "@/hooks/useNostr";
import { getSatAmountFromInvoice } from "@/utils/lightning";
+import ZapDisplay from "@/components/zaps/ZapDisplay";
const CourseTemplate = (course) => {
- const [zaps, setZaps] = useState([]);
- const [zapAmount, setZapAmount] = useState(null);
- const router = useRouter();
- const { returnImageProxy } = useImageProxy();
- const { fetchZapsForEvent } = useNostr();
+ const [zaps, setZaps] = useState([]);
+ const [zapAmount, setZapAmount] = useState(null);
+ const router = useRouter();
+ const { returnImageProxy } = useImageProxy();
+ const { fetchZapsForEvent } = useNostr();
- useEffect(() => {
- const fetchZaps = async () => {
- try {
- const zaps = await fetchZapsForEvent(course.id);
- setZaps(zaps);
- } catch (error) {
- console.error("Error fetching zaps:", error);
- }
- };
- fetchZaps();
- }, [fetchZapsForEvent, course]);
+ useEffect(() => {
+ const fetchZaps = async () => {
+ try {
+ const zaps = await fetchZapsForEvent(course);
+ // console.log('zaps:', zaps);
+ if (zaps.length > 0) {
+ let total = 0;
+ zaps.map((zap) => {
+ 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);
+ }
+ } catch (error) {
+ console.error("Error fetching zaps:", error);
+ }
+ };
+ fetchZaps();
+ }, [fetchZapsForEvent, course]);
- useEffect(() => {
- if (zaps.length > 0) {
- zaps.map((zap) => {
- const bolt11Tag = zap.tags.find((tag) => tag[0] === "bolt11");
- const invoice = bolt11Tag ? bolt11Tag[1] : null;
- if (invoice) {
- const amount = getSatAmountFromInvoice(invoice);
- setZapAmount(zapAmount + amount);
- }
- });
- }
- }, [zaps]);
-
- return (
-
-
router.push(`/details/${course.id}`)}
- className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300"
- style={{ paddingBottom: "56.25%"}}
+ return (
+
-
-
-
-
- {course.title}
-
-
{course.summary}
-
-
- {formatTimestampToHowLongAgo(course.published_at)}
-
-
- {zapAmount}
-
+
router.push(`/details/${course.id}`)}
+ className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
+ style={{ paddingBottom: "56.25%" }}
+ >
+
+
+
+
+ {course.title}
+
+
{course.summary}
+
+
+ {formatTimestampToHowLongAgo(course.published_at)}
+
+
+
+
-
-
- );
+ );
};
export default CourseTemplate;
\ No newline at end of file
diff --git a/src/components/content/carousels/templates/ResourceTemplate.js b/src/components/content/carousels/templates/ResourceTemplate.js
index acac2e4..e21a3fc 100644
--- a/src/components/content/carousels/templates/ResourceTemplate.js
+++ b/src/components/content/carousels/templates/ResourceTemplate.js
@@ -16,7 +16,7 @@ const ResourceTemplate = (resource) => {
useEffect(() => {
const fetchZaps = async () => {
try {
- const zaps = await fetchZapsForEvent(resource.id);
+ const zaps = await fetchZapsForEvent(resource);
setZaps(zaps);
} catch (error) {
console.error("Error fetching zaps:", error);
@@ -45,7 +45,7 @@ const ResourceTemplate = (resource) => {
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
router.push(`/details/${resource.id}`)}
- className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300"
+ className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
style={{ paddingBottom: "56.25%"}}
>
{
useEffect(() => {
const fetchZaps = async () => {
try {
- const zaps = await fetchZapsForEvent(workshop.id);
+ const zaps = await fetchZapsForEvent(workshop);
setZaps(zaps);
} catch (error) {
console.error("Error fetching zaps:", error);
@@ -44,7 +44,7 @@ const WorkshopTemplate = (workshop) => {
>
router.push(`/details/${workshop.id}`)}
- className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300"
+ className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
style={{ paddingBottom: "56.25%"}}
>
{
+ const op = useRef(null);
+ return (
+ <>
+ op.current.toggle(e)}>
+ {zapAmount}
+
+
+
+
+ >
+ )
+}
+
+export default ZapDisplay;
\ No newline at end of file
diff --git a/src/components/zaps/ZapForm.js b/src/components/zaps/ZapForm.js
new file mode 100644
index 0000000..4c915f3
--- /dev/null
+++ b/src/components/zaps/ZapForm.js
@@ -0,0 +1,49 @@
+import React, { useState } from "react";
+import { Button } from 'primereact/button';
+import { InputText } from 'primereact/inputtext';
+import { InputTextarea } from 'primereact/inputtextarea';
+import { useNostr } from "@/hooks/useNostr";
+
+const ZapForm = ({event}) => {
+ const [zapAmount, setZapAmount] = useState(0);
+ const [comment, setComment] = useState("");
+
+ const { zapEvent } = useNostr();
+
+ const handleZapButton = (amount) => {
+ setZapAmount(amount);
+ };
+
+ const handleCustomAmountChange = (event) => {
+ setZapAmount(event.target.value);
+ };
+
+ const handleCommentChange = (event) => {
+ setComment(event.target.value);
+ };
+
+ const handleSubmit = async () => {
+ const millisatAmount = zapAmount * 1000;
+ const response = await zapEvent(event, millisatAmount, comment);
+
+ console.log('zap response:', response);
+ };
+
+ return (
+
+
+ {[1, 10, 21, 100, 500, 1000].map(amount => (
+
+
+
+
+
+
+
+ );
+};
+
+export default ZapForm;
diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js
index 3fcf945..38e8f8e 100644
--- a/src/hooks/useNostr.js
+++ b/src/hooks/useNostr.js
@@ -1,7 +1,8 @@
import { useState, useEffect, useCallback, useContext } from 'react';
import axios from 'axios';
-import { nip57 } from 'nostr-tools';
+import { nip57, nip19 } from 'nostr-tools';
import { NostrContext } from '@/context/NostrContext';
+import { lnurlEncode } from '@/utils/lnurl';
const defaultRelays = [
"wss://nos.lol/",
@@ -11,19 +12,19 @@ const defaultRelays = [
"wss://nostr.mutinywallet.com/",
"wss://relay.mutinywallet.com/",
"wss://relay.primal.net/"
- ];
+];
export function useNostr() {
const pool = useContext(NostrContext);
const subscribe = useCallback(
(filters, opts) => {
- if (!pool) return;
-
- return pool.subscribeMany(defaultRelays, filters, opts);
+ if (!pool) return;
+
+ return pool.subscribeMany(defaultRelays, filters, opts);
},
[pool]
- );
+ );
const publish = useCallback(
async (event) => {
@@ -55,11 +56,16 @@ export function useNostr() {
);
const fetchZapsForEvent = useCallback(
- async (id) => {
+ async (event) => {
try {
- const filter = { kinds: [9735], '#e': [id] };
- const zaps = await pool.querySync(defaultRelays, filter);
- console.log('zaps:', zaps);
+ let zaps = [];
+ const paramaterizedFilter = { kinds: [9735], '#a': [`${event.kind}:${event.id}:${event.d}`] };
+ const paramaterizedZaps = await pool.querySync(defaultRelays, paramaterizedFilter);
+ console.log('paramaterizedZaps:', paramaterizedZaps);
+ const filter = { kinds: [9735], '#e': [event.id] };
+ const zapsForEvent = await pool.querySync(defaultRelays, filter);
+ console.log('zapsForEvent:', zapsForEvent);
+ zaps = zaps.concat(paramaterizedZaps, zapsForEvent);
return zaps;
} catch (error) {
console.error('Failed to fetch zaps for event:', error);
@@ -84,7 +90,7 @@ export function useNostr() {
);
const zapEvent = useCallback(
- async (event) => {
+ async (event, amount, comment) => {
const kind0 = await fetchKind0(event.pubkey);
if (kind0.length === 0) {
@@ -104,16 +110,41 @@ export function useNostr() {
const zapReq = nip57.makeZapRequest({
profile: event.pubkey,
event: event.id,
- amount: 1000,
+ amount: amount,
relays: defaultRelays,
- comment: 'Plebdevs Zap',
+ comment: comment ? comment : 'Plebdevs Zap',
});
- console.log('zapReq:', zapReq);
+ const user = window.localStorage.getItem('user');
+
+ const pubkey = JSON.parse(user).pubkey;
+
+ const lnurl = lnurlEncode(lud16Url)
+
+ console.log('lnurl:', lnurl);
+
+ console.log('pubkey:', pubkey);
+
+ // const zapRequest = {
+ // kind: 9734,
+ // content: "",
+ // tags: [
+ // ["relays", defaultRelays[4], defaultRelays[5]],
+ // ["amount", amount.toString()],
+ // // ["lnurl", lnurl],
+ // ["e", event.id],
+ // ["p", event.pubkey],
+ // // ["a", `${event.kind}:${event.id}:${event.d}`],
+ // ],
+ // created_at: Math.floor(Date.now() / 1000)
+ // }
+
+ console.log('zapRequest:', zapReq);
const signedEvent = await window?.nostr?.signEvent(zapReq);
+ console.log('signedEvent:', signedEvent);
const callbackUrl = response.data.callback;
- const zapRequestAPICall = `${callbackUrl}?amount=${1000}&nostr=${encodeURI(
+ const zapRequestAPICall = `${callbackUrl}?amount=${amount}&nostr=${encodeURI(
JSON.stringify(signedEvent)
)}`;
@@ -149,10 +180,10 @@ export function useNostr() {
const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource");
return hasPlebDevs && hasResource;
};
-
+
return new Promise((resolve, reject) => {
let resources = [];
-
+
const subscription = subscribe(
filter,
{
@@ -172,14 +203,14 @@ export function useNostr() {
},
2000 // Adjust the timeout value as needed
);
-
+
setTimeout(() => {
subscription?.close();
resolve(resources);
}, 2000); // Adjust the timeout value as needed
});
}, [subscribe]);
-
+
const fetchWorkshops = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const hasRequiredTags = (eventData) => {
@@ -187,10 +218,10 @@ export function useNostr() {
const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop");
return hasPlebDevs && hasWorkshop;
};
-
+
return new Promise((resolve, reject) => {
let workshops = [];
-
+
const subscription = subscribe(
filter,
{
@@ -210,14 +241,14 @@ export function useNostr() {
},
2000 // Adjust the timeout value as needed
);
-
+
setTimeout(() => {
subscription?.close();
resolve(workshops);
}, 2000); // Adjust the timeout value as needed
});
}, [subscribe]);
-
+
const fetchCourses = useCallback(async () => {
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
const hasRequiredTags = (eventData) => {
@@ -225,10 +256,10 @@ export function useNostr() {
const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course");
return hasPlebDevs && hasCourse;
};
-
+
return new Promise((resolve, reject) => {
let courses = [];
-
+
const subscription = subscribe(
filter,
{
@@ -248,7 +279,7 @@ export function useNostr() {
},
2000 // Adjust the timeout value as needed
);
-
+
setTimeout(() => {
subscription?.close();
resolve(courses);
diff --git a/src/utils/lnurl.js b/src/utils/lnurl.js
new file mode 100644
index 0000000..39f1b88
--- /dev/null
+++ b/src/utils/lnurl.js
@@ -0,0 +1,12 @@
+import {bech32} from 'bech32';
+
+export const lnurlEncode = (data) => {
+ console.log('data:', data);
+ const words = bech32.toWords(Buffer.from(data, 'utf8'));
+ return bech32.encode("lnurl", words, 2000).toUpperCase()
+};
+
+export const lnurlDecode = (encoded) => {
+ const { words } = bech32.decode(encoded, 90);
+ return Buffer.from(bech32.fromWords(words)).toString('utf8');
+};
\ No newline at end of file
diff --git a/src/utils/nostr.js b/src/utils/nostr.js
index c885425..ddb42d3 100644
--- a/src/utils/nostr.js
+++ b/src/utils/nostr.js
@@ -35,6 +35,7 @@ export const parseEvent = (event) => {
id: event.id,
pubkey: event.pubkey || '',
content: event.content || '',
+ kind: event.kind || '',
title: '',
summary: '',
image: '',
@@ -59,6 +60,9 @@ export const parseEvent = (event) => {
case 'author':
eventData.author = tag[1];
break;
+ case 'd':
+ eventData.d = tag[1];
+ break;
default:
break;
}