chore(buddychain): improvements (#104)

This commit is contained in:
Danish Arora 2024-10-28 18:23:21 +05:30 committed by GitHub
parent 49460a2f62
commit ac506bcf5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 250 additions and 147 deletions

11
ci/Jenkinsfile vendored
View File

@ -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() } } }
}
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Buddychain Dogfood</title>
<title>BuddyBook Dogfood</title>
</head>
<body>
<div id="root"></div>

View File

@ -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 (
<div className="min-h-screen bg-background text-foreground">
<Header wakuStatus={wakuStatus} />
<main className="container mx-auto px-4 py-8">
<main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl">
<Routes>
<Route path="/create" element={<ChainCreationForm />} />
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
@ -132,22 +127,20 @@ function App() {
}
const Home: React.FC = () => (
<div className="space-y-6 text-center">
<h1 className="text-4xl font-bold">BuddyChain</h1>
<div className="max-w-md mx-auto p-6 bg-card rounded-lg shadow-md">
<div className="space-y-4 md:space-y-6 p-4 md:p-6">
<h1 className="text-2xl md:text-4xl font-bold">BuddyBook</h1>
<div className="w-full max-w-sm mx-auto p-4 md:p-6 bg-card rounded-lg shadow-md">
<Link to="/create">
<Button
className="w-full mb-4"
>
<Button className="w-full mb-4">
Create New Chain
</Button>
</Link>
<p className="text-muted-foreground">
<p className="text-sm md:text-base text-muted-foreground">
Click the button above to start creating a new chain.
</p>
</div>
<p className="text-sm text-muted-foreground">
Welcome to BuddyChain - Create and share your chains!
<p className="text-xs md:text-sm text-muted-foreground text-center">
Welcome to BuddyBook - Create and share your chains!
</p>
</div>
)

View File

@ -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<SignChainProps> = ({ block, onSuccess }) => {
const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) => {
const [isOpen, setIsOpen] = useState(false);
const [isSigning, setIsSigning] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -25,17 +26,37 @@ const SignChain: React.FC<SignChainProps> = ({ 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<SignChainProps> = ({ 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<SignChainProps> = ({ block, onSuccess }) => {
{alreadySigned ? 'Already Signed' : 'Sign Chain'}
</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Sign Chain</DialogTitle>
<DialogDescription>
@ -111,7 +133,14 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
: 'Review the block details and sign to add your signature to the chain.'}
</DialogDescription>
</DialogHeader>
<QRCode data={block} />
<div className="flex flex-col space-y-4">
<div className="space-y-2">
<h4 className="font-medium">Block Details</h4>
<p className="text-sm text-muted-foreground">{block.title}</p>
<p className="text-sm text-muted-foreground">{block.description}</p>
</div>
<QRCode text={`${window.location.origin}/sign/${block.chainUUID}/${block.blockUUID}`} />
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<DialogFooter>
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>

View File

@ -44,7 +44,11 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
<CardContent>
<h2 className="text-xl font-semibold mb-2">{block.title}</h2>
<p className="mb-4">{block.description}</p>
<SignChain block={block} onSuccess={onChainUpdate} />
<SignChain
block={block}
chainsData={chainsData}
onSuccess={onChainUpdate}
/>
</CardContent>
</Card>
);

View File

@ -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<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
Block UUID: {block.blockUUID}
</p>
<div className="mt-2 space-x-2">
<SignChain block={block} onSuccess={handleChainUpdate} />
<SignChain
block={block}
chainsData={chainsData}
onSuccess={handleChainUpdate}
/>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Share</Button>
@ -58,6 +62,9 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share this Chain</DialogTitle>
<DialogDescription>
Share this chain with others to collect their signatures.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center space-y-4">
<QRCode text={shareUrl} width={200} height={200} />

View File

@ -56,69 +56,78 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
};
return (
<header className="bg-background border-b border-border">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-bold">BuddyBook</h1>
<nav>
<ul className="flex space-x-4">
<li>
<Link
to="/create"
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Create Chain
</Link>
</li>
<li>
<Link
to="/view"
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
View Existing Chains
</Link>
</li>
<li>
<Link
to="/telemetry"
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Telemetry
</Link>
</li>
</ul>
</nav>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<span className="text-sm text-muted-foreground">Filter:</span>
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
<header className="border-b">
<div className="container mx-auto px-4 py-2 md:py-4">
<div className="flex flex-col md:flex-row justify-between items-center space-y-2 md:space-y-0">
<div className="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4 w-full md:w-auto">
<h1 className="text-xl md:text-2xl font-bold">BuddyBook</h1>
<nav className="w-full md:w-auto">
<ul className="flex justify-center md:justify-start space-x-4">
<li>
<Link
to="/create"
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Create Chain
</Link>
</li>
<li>
<Link
to="/view"
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
View Chains
</Link>
</li>
<li>
<Link
to="/telemetry"
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Telemetry
</Link>
</li>
</ul>
</nav>
</div>
<div className="flex items-center space-x-1">
<span className="text-sm text-muted-foreground">Store:</span>
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
<div className="flex flex-wrap justify-center md:justify-end items-center gap-2 w-full md:w-auto">
<div className="flex items-center space-x-2 text-xs md:text-sm">
<div className="flex items-center space-x-1">
<span className="text-muted-foreground">Filter:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
</div>
<div className="flex items-center space-x-1">
<span className="text-muted-foreground">Store:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
</div>
</div>
<div className="flex items-center space-x-2">
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-xs md:text-sm text-red-500">Waku Error</span>
) : (
<span className="text-xs md:text-sm text-muted-foreground hidden md:inline">
Waku Connections: {connections}
</span>
)}
{isConnected ? (
<div className="flex items-center space-x-2">
<span className="text-xs md:text-sm text-muted-foreground truncate max-w-[120px] md:max-w-none">
{ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')}
</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
Logout
</Button>
</div>
) : (
<ConnectKitButton />
)}
</div>
</div>
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-sm text-red-500">Waku Error</span>
) : (
<span className="text-sm text-muted-foreground">
Waku Connections: {connections}
</span>
)}
{isConnected ? (
<>
<span className="text-sm text-muted-foreground">
{ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')}
</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
Logout
</Button>
</>
) : (
<ConnectKitButton />
)}
</div>
</div>
</header>

View File

@ -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<QRCodeProps> = ({ 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 (
<div className="flex flex-col items-center space-y-4">
<QRCodeSVG value={text} size={Math.min(width, height)} />
<div className="flex items-center space-x-2">
<input
type="text"
value={text}
readOnly
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopy}
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
);
};

View File

@ -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<TelemetryOptInProps> = ({ onOptIn }) => {
const PrivacyPolicyOptIn: React.FC<PrivacyPolicyOptInProps> = ({ onOptIn }) => {
const [showFullPolicy, setShowFullPolicy] = useState(false);
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Telemetry Data Collection</CardTitle>
<CardHeader className="space-y-2">
<CardTitle className="text-xl sm:text-2xl">Privacy Policy & Data Collection</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
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.
</p>
<Button variant="link" onClick={() => setShowFullPolicy(true)}>
View Full Privacy Policy
</Button>
<div className="space-y-4">
<p className="text-sm sm:text-base text-muted-foreground">
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.
</p>
<Button
variant="link"
onClick={() => setShowFullPolicy(true)}
className="px-0 text-sm sm:text-base"
>
View Full Privacy Policy
</Button>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={() => onOptIn(false)}>Opt Out</Button>
<Button onClick={() => onOptIn(true)}>Opt In</Button>
<CardFooter className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<Button
variant="outline"
onClick={() => onOptIn(false)}
className="w-full sm:w-auto"
>
Opt Out
</Button>
<Button
onClick={() => onOptIn(true)}
className="w-full sm:w-auto"
>
Opt In
</Button>
</CardFooter>
</Card>
<Dialog open={showFullPolicy} onOpenChange={setShowFullPolicy}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogContent className="w-[95vw] max-w-4xl max-h-[90vh] p-4 sm:p-6">
<DialogHeader>
<DialogTitle>Privacy Policy</DialogTitle>
<DialogTitle className="text-xl sm:text-2xl">Privacy Policy</DialogTitle>
</DialogHeader>
<ScrollArea className="mt-4 h-[60vh]">
<ScrollArea className="mt-4 h-[50vh] sm:h-[60vh]">
<DialogDescription className="space-y-4">
<ReactMarkdown className="prose dark:prose-invert max-w-none">
<ReactMarkdown className="prose dark:prose-invert max-w-none text-sm sm:text-base">
{privacyPolicy}
</ReactMarkdown>
</DialogDescription>
@ -51,4 +68,4 @@ const TelemetryOptIn: React.FC<TelemetryOptInProps> = ({ onOptIn }) => {
);
};
export default TelemetryOptIn;
export default PrivacyPolicyOptIn;

View File

@ -6,44 +6,49 @@ import { privacyPolicy } from '@/lib/privacyPolicy';
import ReactMarkdown from 'react-markdown';
const TelemetryPage: React.FC = () => {
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean>(false);
const PrivacyPolicyPage: React.FC = () => {
const [privacyPolicyOptIn, setPrivacyPolicyOptIn] = useState<boolean>(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 (
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle>Telemetry Settings</CardTitle>
<Card className="w-full max-w-4xl mx-auto p-4 sm:p-6">
<CardHeader className="space-y-2">
<CardTitle className="text-2xl sm:text-3xl">Privacy Policy Settings</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<p className="text-sm text-muted-foreground mb-2">
We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used.
<div className="space-y-6 sm:space-y-8">
<div className="space-y-4">
<p className="text-sm sm:text-base text-muted-foreground">
We collect data to improve our services. This data is anonymous and helps us understand how our application is used.
</p>
<p className="font-semibold mb-2">
Current status: {telemetryOptIn ? 'Opted In' : 'Opted Out'}
</p>
<Button onClick={handleToggleTelemetry}>
{telemetryOptIn ? 'Opt Out' : 'Opt In'}
</Button>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<p className="font-semibold">
Current status: {privacyPolicyOptIn ? 'Opted In' : 'Opted Out'}
</p>
<Button
onClick={handleTogglePrivacyPolicy}
className="w-full sm:w-auto"
>
{privacyPolicyOptIn ? 'Opt Out' : 'Opt In'}
</Button>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Privacy Policy</h3>
<ScrollArea className="h-[60vh] border rounded-md p-4">
<ReactMarkdown className="prose dark:prose-invert max-w-none">
<h3 className="text-lg sm:text-xl font-semibold mb-4">Privacy Policy</h3>
<ScrollArea className="h-[50vh] sm:h-[60vh] border rounded-md p-2 sm:p-4">
<ReactMarkdown className="prose dark:prose-invert max-w-none text-sm sm:text-base">
{privacyPolicy}
</ReactMarkdown>
</ScrollArea>
@ -54,4 +59,4 @@ const TelemetryPage: React.FC = () => {
);
};
export default TelemetryPage;
export default PrivacyPolicyPage;

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
"rounded-lg border bg-card text-card-foreground shadow-sm p-4 md:p-6",
className
)}
{...props}
@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
className={cn("flex flex-col space-y-1.5 p-4 md:p-6", className)}
{...props}
/>
))

View File

@ -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;
}
}
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;
}
}

View File

@ -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,

View File

@ -12,4 +12,4 @@ export const config = createConfig(
[mainnet.id]: http(),
},
}),
)
)

View File

@ -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