diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile index 8b207e7..0649381 100644 --- a/ci/Jenkinsfile +++ b/ci/Jenkinsfile @@ -44,16 +44,7 @@ pipeline { stage('dogfooding') { steps { script { buildExample() } } } stage('message-monitor') { steps { script { buildExample() } } } stage('flush-notes') { steps { script { buildNextJSExample() } } } - stage('buddybook') { - steps { - script { - dir('examples/buddybook') { - sh 'npm install' - sh 'npm run build:ci' - } - } - } - } + stage('buddybook') { steps { script { buildExample() } } } } } diff --git a/examples/buddybook/index.html b/examples/buddybook/index.html index 6d4c0b5..822d00d 100644 --- a/examples/buddybook/index.html +++ b/examples/buddybook/index.html @@ -4,7 +4,7 @@ - Buddychain Dogfood + BuddyBook Dogfood
diff --git a/examples/buddybook/src/App.tsx b/examples/buddybook/src/App.tsx index 629b6eb..f74a514 100644 --- a/examples/buddybook/src/App.tsx +++ b/examples/buddybook/src/App.tsx @@ -41,13 +41,8 @@ function App() { useEffect(() => { if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return; - - setTimeout(() => { setIsListening(true); startMessageListening(); - }, 3000); - - }, [node, isWakuLoading, wakuStatus]) const handleTelemetryOptIn = (optIn: boolean) => { @@ -117,7 +112,7 @@ function App() { return (
-
+
} /> } /> @@ -132,22 +127,20 @@ function App() { } const Home: React.FC = () => ( -
-

BuddyChain

-
+
+

BuddyBook

+
- -

+

Click the button above to start creating a new chain.

-

- Welcome to BuddyChain - Create and share your chains! +

+ Welcome to BuddyBook - Create and share your chains!

) diff --git a/examples/buddybook/src/components/Chain/SignChain.tsx b/examples/buddybook/src/components/Chain/SignChain.tsx index 773bc8a..6bbfeb2 100644 --- a/examples/buddybook/src/components/Chain/SignChain.tsx +++ b/examples/buddybook/src/components/Chain/SignChain.tsx @@ -11,10 +11,11 @@ import { v4 as uuidv4 } from 'uuid'; interface SignChainProps { block: BlockPayload; + chainsData: BlockPayload[]; // Add this prop onSuccess: (newBlock: BlockPayload) => void; } -const SignChain: React.FC = ({ block, onSuccess }) => { +const SignChain: React.FC = ({ block, chainsData, onSuccess }) => { const [isOpen, setIsOpen] = useState(false); const [isSigning, setIsSigning] = useState(false); const [error, setError] = useState(null); @@ -25,17 +26,37 @@ const SignChain: React.FC = ({ block, onSuccess }) => { useEffect(() => { if (address) { - const hasAlreadySigned = block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase()); + // Check if the address has signed this block or any blocks in the chain + const checkSignatures = (blockToCheck: BlockPayload): boolean => { + // Check current block's signatures + if (blockToCheck.signatures.some( + sig => sig.address.toLowerCase() === address.toLowerCase() + )) { + return true; + } + + // Check parent blocks + const parentBlock = chainsData.find(b => b.blockUUID === blockToCheck.parentBlockUUID); + if (parentBlock && checkSignatures(parentBlock)) { + return true; + } + + // Check child blocks + const childBlocks = chainsData.filter(b => b.parentBlockUUID === blockToCheck.blockUUID); + return childBlocks.some(childBlock => checkSignatures(childBlock)); + }; + + const hasAlreadySigned = checkSignatures(block); setAlreadySigned(hasAlreadySigned); } - }, [address, block.signatures]); + }, [address, block, chainsData]); const { signMessage } = useSignMessage({ mutation: { async onSuccess(signature) { if (!address || !node) return; - // Check if the address has already signed + // Double check signature before proceeding if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) { setError('You have already signed this chain.'); setIsSigning(false); @@ -79,6 +100,7 @@ const SignChain: React.FC = ({ block, onSuccess }) => { }); const handleSign = () => { + // Add an additional check here before signing if (alreadySigned) { setError('You have already signed this chain.'); return; @@ -102,7 +124,7 @@ const SignChain: React.FC = ({ block, onSuccess }) => { {alreadySigned ? 'Already Signed' : 'Sign Chain'} - + Sign Chain @@ -111,7 +133,14 @@ const SignChain: React.FC = ({ block, onSuccess }) => { : 'Review the block details and sign to add your signature to the chain.'} - +
+
+

Block Details

+

{block.title}

+

{block.description}

+
+ +
{error &&

{error}

} diff --git a/examples/buddybook/src/components/Chain/SignSharedChain.tsx b/examples/buddybook/src/components/Chain/SignSharedChain.tsx index 867786f..a57f6bc 100644 --- a/examples/buddybook/src/components/Chain/SignSharedChain.tsx +++ b/examples/buddybook/src/components/Chain/SignSharedChain.tsx @@ -44,7 +44,11 @@ const SignSharedChain: React.FC = ({ chainsData, onChainUp

{block.title}

{block.description}

- +
); diff --git a/examples/buddybook/src/components/Chain/View/ChainList.tsx b/examples/buddybook/src/components/Chain/View/ChainList.tsx index 39de0c7..cbdd84e 100644 --- a/examples/buddybook/src/components/Chain/View/ChainList.tsx +++ b/examples/buddybook/src/components/Chain/View/ChainList.tsx @@ -4,7 +4,7 @@ import { type BlockPayload } from '@/lib/waku'; import SignChain from '@/components/Chain/SignChain'; import { useEnsName } from 'wagmi'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import QRCode from '@/components/QRCode'; import { Loader2 } from "lucide-react"; @@ -50,7 +50,11 @@ const ChainList: React.FC = ({ chainsData, onChainUpdate, isLoad Block UUID: {block.blockUUID}

- + @@ -58,6 +62,9 @@ const ChainList: React.FC = ({ chainsData, onChainUpdate, isLoad Share this Chain + + Share this chain with others to collect their signatures. +
diff --git a/examples/buddybook/src/components/Header.tsx b/examples/buddybook/src/components/Header.tsx index b2ea92f..1d9c093 100644 --- a/examples/buddybook/src/components/Header.tsx +++ b/examples/buddybook/src/components/Header.tsx @@ -56,69 +56,78 @@ const Header: React.FC = ({ wakuStatus }) => { }; return ( -
-
-
-

BuddyBook

- -
-
-
- Filter: -
+
+
+
+
+

BuddyBook

+
-
- Store: -
+ +
+
+
+ Filter: +
+
+
+ Store: +
+
+
+ +
+ {isWakuLoading ? ( + + ) : wakuError ? ( + Waku Error + ) : ( + + Waku Connections: {connections} + + )} + + {isConnected ? ( +
+ + {ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')} + + +
+ ) : ( + + )} +
- {isWakuLoading ? ( - - ) : wakuError ? ( - Waku Error - ) : ( - - Waku Connections: {connections} - - )} - {isConnected ? ( - <> - - {ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')} - - - - ) : ( - - )}
diff --git a/examples/buddybook/src/components/QRCode.tsx b/examples/buddybook/src/components/QRCode.tsx index a8b24c7..3184d4e 100644 --- a/examples/buddybook/src/components/QRCode.tsx +++ b/examples/buddybook/src/components/QRCode.tsx @@ -1,5 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { QRCodeSVG } from 'qrcode.react'; +import { Button } from "@/components/ui/button"; +import { Check, Copy } from "lucide-react"; interface QRCodeProps { text: string; @@ -8,9 +10,32 @@ interface QRCodeProps { } const QRCode: React.FC = ({ text, width = 256, height = 256 }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return (
+
+ + +
); }; diff --git a/examples/buddybook/src/components/TelemetryOptIn.tsx b/examples/buddybook/src/components/TelemetryOptIn.tsx index 8dd1cf9..4222d8a 100644 --- a/examples/buddybook/src/components/TelemetryOptIn.tsx +++ b/examples/buddybook/src/components/TelemetryOptIn.tsx @@ -6,41 +6,58 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { privacyPolicy } from '@/lib/privacyPolicy'; import ReactMarkdown from 'react-markdown'; -interface TelemetryOptInProps { +interface PrivacyPolicyOptInProps { onOptIn: (optIn: boolean) => void; } -const TelemetryOptIn: React.FC = ({ onOptIn }) => { +const PrivacyPolicyOptIn: React.FC = ({ onOptIn }) => { const [showFullPolicy, setShowFullPolicy] = useState(false); return ( -
+
- - Telemetry Data Collection + + Privacy Policy & Data Collection -

- We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used. You can opt-in or opt-out of this data collection. -

- +
+

+ We collect data to improve our services. This data is anonymous and helps us understand how our application is used. You can opt-in or opt-out of this data collection. +

+ +
- - - + + +
- + - Privacy Policy + Privacy Policy - + - + {privacyPolicy} @@ -51,4 +68,4 @@ const TelemetryOptIn: React.FC = ({ onOptIn }) => { ); }; -export default TelemetryOptIn; +export default PrivacyPolicyOptIn; diff --git a/examples/buddybook/src/components/TelemetryPage.tsx b/examples/buddybook/src/components/TelemetryPage.tsx index 2a0d958..e8c9695 100644 --- a/examples/buddybook/src/components/TelemetryPage.tsx +++ b/examples/buddybook/src/components/TelemetryPage.tsx @@ -6,44 +6,49 @@ import { privacyPolicy } from '@/lib/privacyPolicy'; import ReactMarkdown from 'react-markdown'; -const TelemetryPage: React.FC = () => { - const [telemetryOptIn, setTelemetryOptIn] = useState(false); +const PrivacyPolicyPage: React.FC = () => { + const [privacyPolicyOptIn, setPrivacyPolicyOptIn] = useState(false); useEffect(() => { - const storedOptIn = localStorage.getItem('telemetryOptIn'); + const storedOptIn = localStorage.getItem('privacyPolicyOptIn'); if (storedOptIn !== null) { - setTelemetryOptIn(storedOptIn === 'true'); + setPrivacyPolicyOptIn(storedOptIn === 'true'); } }, []); - const handleToggleTelemetry = () => { - const newOptIn = !telemetryOptIn; - setTelemetryOptIn(newOptIn); - localStorage.setItem('telemetryOptIn', newOptIn.toString()); + const handleTogglePrivacyPolicy = () => { + const newOptIn = !privacyPolicyOptIn; + setPrivacyPolicyOptIn(newOptIn); + localStorage.setItem('privacyPolicyOptIn', newOptIn.toString()); }; return ( - - - Telemetry Settings + + + Privacy Policy Settings -
-
-

- We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used. +

+
+

+ We collect data to improve our services. This data is anonymous and helps us understand how our application is used.

-

- Current status: {telemetryOptIn ? 'Opted In' : 'Opted Out'} -

- +
+

+ Current status: {privacyPolicyOptIn ? 'Opted In' : 'Opted Out'} +

+ +
-

Privacy Policy

- - +

Privacy Policy

+ + {privacyPolicy} @@ -54,4 +59,4 @@ const TelemetryPage: React.FC = () => { ); }; -export default TelemetryPage; +export default PrivacyPolicyPage; diff --git a/examples/buddybook/src/components/ui/card.tsx b/examples/buddybook/src/components/ui/card.tsx index 77e9fb7..94c9736 100644 --- a/examples/buddybook/src/components/ui/card.tsx +++ b/examples/buddybook/src/components/ui/card.tsx @@ -9,7 +9,7 @@ const Card = React.forwardRef<
(({ className, ...props }, ref) => (
)) diff --git a/examples/buddybook/src/index.css b/examples/buddybook/src/index.css index 46947be..1d45b6b 100644 --- a/examples/buddybook/src/index.css +++ b/examples/buddybook/src/index.css @@ -56,11 +56,34 @@ --chart-5: 340 75% 55% } } + @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; + } + .container { + @apply px-4 md:px-6 lg:px-8; } -} \ No newline at end of file + + h1 { + @apply text-2xl md:text-4xl font-bold; + } + + h2 { + @apply text-xl md:text-3xl font-semibold; + } + + h3 { + @apply text-lg md:text-2xl font-semibold; + } + .section { + @apply py-4 md:py-6 lg:py-8; + } + + .card { + @apply p-4 md:p-6; + } +} diff --git a/examples/buddybook/src/lib/waku.ts b/examples/buddybook/src/lib/waku.ts index 064d18f..43c1e46 100644 --- a/examples/buddybook/src/lib/waku.ts +++ b/examples/buddybook/src/lib/waku.ts @@ -22,7 +22,7 @@ export type BlockPayload = { parentBlockUUID: string | null; } -const contentTopic = "/buddychain-dogfood/1/chain/proto"; +const contentTopic = "/buddybook-dogfood/1/chain/proto"; export const encoder = createEncoder({ contentTopic: contentTopic, diff --git a/examples/buddybook/src/lib/walletConnect.ts b/examples/buddybook/src/lib/walletConnect.ts index 0950733..e8de5cb 100644 --- a/examples/buddybook/src/lib/walletConnect.ts +++ b/examples/buddybook/src/lib/walletConnect.ts @@ -12,4 +12,4 @@ export const config = createConfig( [mainnet.id]: http(), }, }), - ) \ No newline at end of file + ) diff --git a/examples/buddybook/tasks.md b/examples/buddybook/tasks.md index 05c7274..6118deb 100644 --- a/examples/buddybook/tasks.md +++ b/examples/buddybook/tasks.md @@ -1,6 +1,6 @@ - [ x ] waku connections on header should have green/yellow/red color indicator - [ ] clicking on the indicator should show a list of peers -- [ ] chains can't be signed twice by an address +- [ x ] chains can't be signed twice by an address - [ ] generate waku peer id using the wallet address - [ ] telemetry - [ x ] disclaimer