mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
Working on zaps
This commit is contained in:
parent
edf97c74fa
commit
a255f1e5b0
6
package-lock.json
generated
6
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@prisma/client": "^5.9.1",
|
"@prisma/client": "^5.9.1",
|
||||||
"@reduxjs/toolkit": "^2.1.0",
|
"@reduxjs/toolkit": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"bech32": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"light-bolt11-decoder": "^3.1.1",
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
@ -1378,6 +1379,11 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"@prisma/client": "^5.9.1",
|
"@prisma/client": "^5.9.1",
|
||||||
"@reduxjs/toolkit": "^2.1.0",
|
"@reduxjs/toolkit": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"bech32": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"light-bolt11-decoder": "^3.1.1",
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
|
@ -5,6 +5,7 @@ import { formatTimestampToHowLongAgo } from "@/utils/time";
|
|||||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
import { useNostr } from "@/hooks/useNostr";
|
import { useNostr } from "@/hooks/useNostr";
|
||||||
import { getSatAmountFromInvoice } from "@/utils/lightning";
|
import { getSatAmountFromInvoice } from "@/utils/lightning";
|
||||||
|
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||||
|
|
||||||
const CourseTemplate = (course) => {
|
const CourseTemplate = (course) => {
|
||||||
const [zaps, setZaps] = useState([]);
|
const [zaps, setZaps] = useState([]);
|
||||||
@ -16,8 +17,20 @@ const CourseTemplate = (course) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchZaps = async () => {
|
const fetchZaps = async () => {
|
||||||
try {
|
try {
|
||||||
const zaps = await fetchZapsForEvent(course.id);
|
const zaps = await fetchZapsForEvent(course);
|
||||||
setZaps(zaps);
|
// 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) {
|
} catch (error) {
|
||||||
console.error("Error fetching zaps:", error);
|
console.error("Error fetching zaps:", error);
|
||||||
}
|
}
|
||||||
@ -25,27 +38,14 @@ const CourseTemplate = (course) => {
|
|||||||
fetchZaps();
|
fetchZaps();
|
||||||
}, [fetchZapsForEvent, course]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
|
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => router.push(`/details/${course.id}`)}
|
onClick={() => router.push(`/details/${course.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%"}}
|
style={{ paddingBottom: "56.25%" }}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
alt="course thumbnail"
|
alt="course thumbnail"
|
||||||
@ -65,9 +65,7 @@ const CourseTemplate = (course) => {
|
|||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400">
|
||||||
{formatTimestampToHowLongAgo(course.published_at)}
|
{formatTimestampToHowLongAgo(course.published_at)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs cursor-pointer">
|
<ZapDisplay zapAmount={zapAmount} event={course} />
|
||||||
<i className="pi pi-bolt text-yellow-300"></i> {zapAmount}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@ const ResourceTemplate = (resource) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchZaps = async () => {
|
const fetchZaps = async () => {
|
||||||
try {
|
try {
|
||||||
const zaps = await fetchZapsForEvent(resource.id);
|
const zaps = await fetchZapsForEvent(resource);
|
||||||
setZaps(zaps);
|
setZaps(zaps);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching zaps:", 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 */}
|
{/* 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
|
<div
|
||||||
onClick={() => router.push(`/details/${resource.id}`)}
|
onClick={() => 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%"}}
|
style={{ paddingBottom: "56.25%"}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
@ -16,7 +16,7 @@ const WorkshopTemplate = (workshop) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchZaps = async () => {
|
const fetchZaps = async () => {
|
||||||
try {
|
try {
|
||||||
const zaps = await fetchZapsForEvent(workshop.id);
|
const zaps = await fetchZapsForEvent(workshop);
|
||||||
setZaps(zaps);
|
setZaps(zaps);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching zaps:", error);
|
console.error("Error fetching zaps:", error);
|
||||||
@ -44,7 +44,7 @@ const WorkshopTemplate = (workshop) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => router.push(`/details/${workshop.id}`)}
|
onClick={() => 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%"}}
|
style={{ paddingBottom: "56.25%"}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
19
src/components/zaps/ZapDisplay.js
Normal file
19
src/components/zaps/ZapDisplay.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { OverlayPanel } from 'primereact/overlaypanel';
|
||||||
|
import ZapForm from './ZapForm';
|
||||||
|
|
||||||
|
const ZapDisplay = ({ zapAmount, event }) => {
|
||||||
|
const op = useRef(null);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-xs cursor-pointer" onClick={(e) => op.current.toggle(e)}>
|
||||||
|
<i className="pi pi-bolt text-yellow-300"></i> {zapAmount}
|
||||||
|
</p>
|
||||||
|
<OverlayPanel ref={op}>
|
||||||
|
<ZapForm event={event} />
|
||||||
|
</OverlayPanel>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ZapDisplay;
|
49
src/components/zaps/ZapForm.js
Normal file
49
src/components/zaps/ZapForm.js
Normal file
@ -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 (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row justify-start">
|
||||||
|
{[1, 10, 21, 100, 500, 1000].map(amount => (
|
||||||
|
<Button key={amount} label={amount.toString()} icon="pi pi-bolt" severity="success"
|
||||||
|
rounded className="mr-2" onClick={() => handleZapButton(amount)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row w-[100%] justify-between my-4">
|
||||||
|
<InputText placeholder="Custom Amount" value={zapAmount} onChange={handleCustomAmountChange} />
|
||||||
|
</div>
|
||||||
|
<InputTextarea rows={5} placeholder="Message" value={comment} onChange={handleCommentChange} />
|
||||||
|
<Button label="Zap" icon="pi pi-bolt" severity="success" className="mt-4" onClick={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZapForm;
|
@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback, useContext } from 'react';
|
import { useState, useEffect, useCallback, useContext } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { nip57 } from 'nostr-tools';
|
import { nip57, nip19 } from 'nostr-tools';
|
||||||
import { NostrContext } from '@/context/NostrContext';
|
import { NostrContext } from '@/context/NostrContext';
|
||||||
|
import { lnurlEncode } from '@/utils/lnurl';
|
||||||
|
|
||||||
const defaultRelays = [
|
const defaultRelays = [
|
||||||
"wss://nos.lol/",
|
"wss://nos.lol/",
|
||||||
@ -11,7 +12,7 @@ const defaultRelays = [
|
|||||||
"wss://nostr.mutinywallet.com/",
|
"wss://nostr.mutinywallet.com/",
|
||||||
"wss://relay.mutinywallet.com/",
|
"wss://relay.mutinywallet.com/",
|
||||||
"wss://relay.primal.net/"
|
"wss://relay.primal.net/"
|
||||||
];
|
];
|
||||||
|
|
||||||
export function useNostr() {
|
export function useNostr() {
|
||||||
const pool = useContext(NostrContext);
|
const pool = useContext(NostrContext);
|
||||||
@ -55,11 +56,16 @@ export function useNostr() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchZapsForEvent = useCallback(
|
const fetchZapsForEvent = useCallback(
|
||||||
async (id) => {
|
async (event) => {
|
||||||
try {
|
try {
|
||||||
const filter = { kinds: [9735], '#e': [id] };
|
let zaps = [];
|
||||||
const zaps = await pool.querySync(defaultRelays, filter);
|
const paramaterizedFilter = { kinds: [9735], '#a': [`${event.kind}:${event.id}:${event.d}`] };
|
||||||
console.log('zaps:', zaps);
|
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;
|
return zaps;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch zaps for event:', error);
|
console.error('Failed to fetch zaps for event:', error);
|
||||||
@ -84,7 +90,7 @@ export function useNostr() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const zapEvent = useCallback(
|
const zapEvent = useCallback(
|
||||||
async (event) => {
|
async (event, amount, comment) => {
|
||||||
const kind0 = await fetchKind0(event.pubkey);
|
const kind0 = await fetchKind0(event.pubkey);
|
||||||
|
|
||||||
if (kind0.length === 0) {
|
if (kind0.length === 0) {
|
||||||
@ -104,16 +110,41 @@ export function useNostr() {
|
|||||||
const zapReq = nip57.makeZapRequest({
|
const zapReq = nip57.makeZapRequest({
|
||||||
profile: event.pubkey,
|
profile: event.pubkey,
|
||||||
event: event.id,
|
event: event.id,
|
||||||
amount: 1000,
|
amount: amount,
|
||||||
relays: defaultRelays,
|
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);
|
const signedEvent = await window?.nostr?.signEvent(zapReq);
|
||||||
|
console.log('signedEvent:', signedEvent);
|
||||||
const callbackUrl = response.data.callback;
|
const callbackUrl = response.data.callback;
|
||||||
const zapRequestAPICall = `${callbackUrl}?amount=${1000}&nostr=${encodeURI(
|
const zapRequestAPICall = `${callbackUrl}?amount=${amount}&nostr=${encodeURI(
|
||||||
JSON.stringify(signedEvent)
|
JSON.stringify(signedEvent)
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
|
12
src/utils/lnurl.js
Normal file
12
src/utils/lnurl.js
Normal file
@ -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');
|
||||||
|
};
|
@ -35,6 +35,7 @@ export const parseEvent = (event) => {
|
|||||||
id: event.id,
|
id: event.id,
|
||||||
pubkey: event.pubkey || '',
|
pubkey: event.pubkey || '',
|
||||||
content: event.content || '',
|
content: event.content || '',
|
||||||
|
kind: event.kind || '',
|
||||||
title: '',
|
title: '',
|
||||||
summary: '',
|
summary: '',
|
||||||
image: '',
|
image: '',
|
||||||
@ -59,6 +60,9 @@ export const parseEvent = (event) => {
|
|||||||
case 'author':
|
case 'author':
|
||||||
eventData.author = tag[1];
|
eventData.author = tag[1];
|
||||||
break;
|
break;
|
||||||
|
case 'd':
|
||||||
|
eventData.d = tag[1];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user