From 0c9aac807b5c59827fe464e182cd724914a7308f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 10 Nov 2024 12:24:11 +0700 Subject: [PATCH] the ai addresses feedback from nostr - adds a currency search box - adds a dedicated donate button --- src/App.tsx | 163 ++++++++++++++++++------ src/components/DonateModal.tsx | 222 +++++++++++++++++++++++++++++++++ src/components/InfoModal.tsx | 186 --------------------------- 3 files changed, 344 insertions(+), 227 deletions(-) create mode 100644 src/components/DonateModal.tsx diff --git a/src/App.tsx b/src/App.tsx index 27bc61d..618c814 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import InfoModal from "./components/InfoModal"; +import DonateModal from "./components/DonateModal"; interface Rate { code: string; @@ -35,12 +36,11 @@ function findParityDate( const today = new Date(); // If exactly at parity (within small margin) - if (Math.abs(satPrice - 1) < 0.1) { + if (satPrice > 1 && satPrice < 1.1) { return { type: "now", date: today }; } // Calculate the BTC price needed for this currency to hit parity - const btcPriceRatio = 100_000_000 / btcRate; const usdPriceNeeded = btcUsdRate * btcPriceRatio; @@ -107,6 +107,9 @@ function App() { const [error, setError] = useState(null); const [showHistoric, setShowHistoric] = useState(false); const [isInfoModalOpen, setIsInfoModalOpen] = useState(false); + const [isDonateModalOpen, setIsDonateModalOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const cardRefs = useRef>({}); useEffect(() => { const fetchRates = async () => { @@ -144,6 +147,23 @@ function App() { return () => clearInterval(interval); }, []); + useEffect(() => { + if (searchTerm) { + const matchedRate = rates.find( + (rate) => + rate.code.toLowerCase().includes(searchTerm.toLowerCase()) || + rate.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (matchedRate) { + cardRefs.current[matchedRate.code]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + } + }, [searchTerm, rates]); + if (loading) { return (
@@ -171,10 +191,17 @@ function App() { ); const filteredRates = rates.filter((rate) => { - if (!showHistoric && rate.parityInfo?.type === "past") { - return rate.parityInfo.date >= threeYearsAgo; - } - return true; + const matchesSearch = + searchTerm === "" || + rate.code.toLowerCase().includes(searchTerm.toLowerCase()) || + rate.name.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesHistoric = + !showHistoric && rate.parityInfo?.type === "past" + ? rate.parityInfo.date >= threeYearsAgo + : true; + + return matchesSearch && matchesHistoric; }); const parityCount = rates.filter( @@ -185,46 +212,94 @@ function App() { return (
-
- - +

Bitcoin Purchasing
Power Tracker

- {/* Toggle Switch */} - +
+
+
+ setSearchTerm(e.target.value)} + className="w-full px-4 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + + + +
+
+ +
+ + + + + +
+
@@ -266,6 +341,7 @@ function App() { return (
(cardRefs.current[rate.code] = el)} className={` p-4 rounded-lg shadow-sm transition-transform hover:-translate-y-1 ${bgColorClass} ${textColorClass} @@ -313,6 +389,11 @@ function App() { isOpen={isInfoModalOpen} onClose={() => setIsInfoModalOpen(false)} /> + + setIsDonateModalOpen(false)} + />
); } diff --git a/src/components/DonateModal.tsx b/src/components/DonateModal.tsx new file mode 100644 index 0000000..825b21d --- /dev/null +++ b/src/components/DonateModal.tsx @@ -0,0 +1,222 @@ +import { nwc } from "@getalby/sdk"; +import { launchPaymentModal } from "@getalby/bitcoin-connect"; +import { useState, useEffect } from "react"; + +const DONATION_CONNECTION_SECRET = + "nostr+walletconnect://c8986738660e5e5ee92e21a51e1f2e5915ad7ee9e972f301fc670f8eb47e9bed?relay=wss://relay.getalby.com/v1&secret=4b1295fe100f412a44803def27d5eef6242443cbf1f2b55c557b141e46a736c7"; + +interface DonateModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface Transaction { + amount: number; + description: string; + timestamp: number; +} + +const DonateModal = ({ isOpen, onClose }: DonateModalProps) => { + const [donationStatus, setDonationStatus] = useState(""); + const [balance, setBalance] = useState(null); + const [transactions, setTransactions] = useState([]); + const [donationMessage, setDonationMessage] = useState(""); + const [selectedAmount, setSelectedAmount] = useState(null); + + const fetchBalance = async () => { + try { + const client = new nwc.NWCClient({ + nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, + }); + const balanceResponse = await client.getBalance(); + setBalance(Math.floor(balanceResponse.balance / 1000)); + } catch (error) { + console.error("Error fetching balance:", error); + } + }; + + const fetchTransactions = async () => { + try { + const client = new nwc.NWCClient({ + nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, + }); + const txResponse = await client.listTransactions({}); + const formattedTransactions = txResponse.transactions + .filter((tx) => tx.settled_at) + .map((tx) => ({ + amount: Math.floor(tx.amount / 1000), // Convert millisats to sats + description: tx.description || "", + timestamp: tx.settled_at, + })); + setTransactions(formattedTransactions); + } catch (error) { + console.error("Error fetching transactions:", error); + } + }; + + useEffect(() => { + if (isOpen) { + fetchBalance(); + fetchTransactions(); + } + }, [isOpen]); + + if (!isOpen) return null; + + const initiateDonation = (amount: number) => { + setSelectedAmount(amount); + }; + + const handleDonation = async () => { + if (!selectedAmount) return; + + try { + const client = new nwc.NWCClient({ + nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, + }); + + const transaction = await client.makeInvoice({ + amount: selectedAmount * 1000, + description: donationMessage, + }); + + const { setPaid } = await launchPaymentModal({ + invoice: transaction.invoice, + onPaid: () => { + clearInterval(checkPaymentInterval); + setDonationStatus("Thank you for your donation!"); + fetchBalance(); + fetchTransactions(); + setSelectedAmount(null); + setDonationMessage(""); + }, + }); + + // Set up payment verification interval + const checkPaymentInterval = setInterval(async () => { + try { + // Use NWC to verify payment + const polledTransaction = await client.lookupInvoice({ + invoice: transaction.invoice, + }); + + if (polledTransaction.preimage) { + setPaid({ + preimage: polledTransaction.preimage, + }); + } + } catch (error) { + console.error("Error checking payment status:", error); + } + }, 1000); + + // Clean up interval after 5 minutes (300000ms) if payment not received + setTimeout(() => { + clearInterval(checkPaymentInterval); + }, 300000); + } catch (error) { + console.error("Error processing donation:", error); + setDonationStatus("Error processing donation"); + setTimeout(() => setDonationStatus(""), 3000); + setSelectedAmount(null); + setDonationMessage(""); + } + }; + + return ( +
+
+
+

Support this project

+ +
+
+ {balance !== null && ( +

+ Total donations received: {balance} sats +

+ )} +
+ + + +
+ {selectedAmount && ( +
+ setDonationMessage(e.target.value)} + placeholder="Add a message (optional)" + className="w-full px-4 py-2 border border-gray-300 rounded-lg mb-2" + /> + +
+ )} + {donationStatus && ( +
+ {donationStatus} +
+ )} + + {transactions.length > 0 && ( +
+

Recent Donations

+
+ {transactions.map((tx, index) => ( +
+ {tx.amount} sats + {tx.description && ( + + "{tx.description}" + + )} +
+ ))} +
+
+ )} +
+
+
+ ); +}; + +export default DonateModal; diff --git a/src/components/InfoModal.tsx b/src/components/InfoModal.tsx index f5019f0..7251627 100644 --- a/src/components/InfoModal.tsx +++ b/src/components/InfoModal.tsx @@ -1,128 +1,11 @@ -import { nwc } from "@getalby/sdk"; -import { launchPaymentModal } from "@getalby/bitcoin-connect"; -import { useState, useEffect } from "react"; - -const DONATION_CONNECTION_SECRET = - "nostr+walletconnect://c8986738660e5e5ee92e21a51e1f2e5915ad7ee9e972f301fc670f8eb47e9bed?relay=wss://relay.getalby.com/v1&secret=4b1295fe100f412a44803def27d5eef6242443cbf1f2b55c557b141e46a736c7"; - interface InfoModalProps { isOpen: boolean; onClose: () => void; } -interface Transaction { - amount: number; - description: string; - timestamp: number; -} - const InfoModal = ({ isOpen, onClose }: InfoModalProps) => { - const [donationStatus, setDonationStatus] = useState(""); - const [balance, setBalance] = useState(null); - const [transactions, setTransactions] = useState([]); - const [donationMessage, setDonationMessage] = useState(""); - const [selectedAmount, setSelectedAmount] = useState(null); - - const fetchBalance = async () => { - try { - const client = new nwc.NWCClient({ - nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, - }); - const balanceResponse = await client.getBalance(); - setBalance(Math.floor(balanceResponse.balance / 1000)); - } catch (error) { - console.error("Error fetching balance:", error); - } - }; - - const fetchTransactions = async () => { - try { - const client = new nwc.NWCClient({ - nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, - }); - const txResponse = await client.listTransactions({}); - const formattedTransactions = txResponse.transactions - .filter((tx) => tx.settled_at) - .map((tx) => ({ - amount: Math.floor(tx.amount / 1000), // Convert millisats to sats - description: tx.description || "", - timestamp: tx.settled_at, - })); - setTransactions(formattedTransactions); - } catch (error) { - console.error("Error fetching transactions:", error); - } - }; - - useEffect(() => { - if (isOpen) { - fetchBalance(); - fetchTransactions(); - } - }, [isOpen]); - if (!isOpen) return null; - const initiateDonation = (amount: number) => { - setSelectedAmount(amount); - }; - - const handleDonation = async () => { - if (!selectedAmount) return; - - try { - const client = new nwc.NWCClient({ - nostrWalletConnectUrl: DONATION_CONNECTION_SECRET, - }); - - const transaction = await client.makeInvoice({ - amount: selectedAmount * 1000, - description: donationMessage, - }); - - const { setPaid } = await launchPaymentModal({ - invoice: transaction.invoice, - onPaid: () => { - clearInterval(checkPaymentInterval); - setDonationStatus("Thank you for your donation!"); - fetchBalance(); - fetchTransactions(); - setSelectedAmount(null); - setDonationMessage(""); - }, - }); - - // Set up payment verification interval - const checkPaymentInterval = setInterval(async () => { - try { - // Use NWC to verify payment - const polledTransaction = await client.lookupInvoice({ - invoice: transaction.invoice, - }); - - if (polledTransaction.preimage) { - setPaid({ - preimage: polledTransaction.preimage, - }); - } - } catch (error) { - console.error("Error checking payment status:", error); - } - }, 1000); - - // Clean up interval after 5 minutes (300000ms) if payment not received - setTimeout(() => { - clearInterval(checkPaymentInterval); - }, 300000); - } catch (error) { - console.error("Error processing donation:", error); - setDonationStatus("Error processing donation"); - setTimeout(() => setDonationStatus(""), 3000); - setSelectedAmount(null); - setDonationMessage(""); - } - }; - return (
@@ -174,75 +57,6 @@ const InfoModal = ({ isOpen, onClose }: InfoModalProps) => { View on GitHub
- -
-

Support this project

- {balance !== null && ( -

- Total donations received: {balance} sats -

- )} -
- - - -
- {selectedAmount && ( -
- setDonationMessage(e.target.value)} - placeholder="Add a message (optional)" - className="w-full px-4 py-2 border border-gray-300 rounded-lg mb-2" - /> - -
- )} - {donationStatus && ( -
- {donationStatus} -
- )} - - {transactions.length > 0 && ( -
-

Recent Donations

-
- {transactions.map((tx, index) => ( -
- {tx.amount} sats - {tx.description && ( - - "{tx.description}" - - )} -
- ))} -
-
- )} -