mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-05 23:33:11 +00:00
chore(buddychain): improvements (#104)
This commit is contained in:
parent
49460a2f62
commit
ac506bcf5a
11
ci/Jenkinsfile
vendored
11
ci/Jenkinsfile
vendored
@ -44,16 +44,7 @@ pipeline {
|
|||||||
stage('dogfooding') { steps { script { buildExample() } } }
|
stage('dogfooding') { steps { script { buildExample() } } }
|
||||||
stage('message-monitor') { steps { script { buildExample() } } }
|
stage('message-monitor') { steps { script { buildExample() } } }
|
||||||
stage('flush-notes') { steps { script { buildNextJSExample() } } }
|
stage('flush-notes') { steps { script { buildNextJSExample() } } }
|
||||||
stage('buddybook') {
|
stage('buddybook') { steps { script { buildExample() } } }
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
dir('examples/buddybook') {
|
|
||||||
sh 'npm install'
|
|
||||||
sh 'npm run build:ci'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Buddychain Dogfood</title>
|
<title>BuddyBook Dogfood</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -41,13 +41,8 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return;
|
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return;
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsListening(true);
|
setIsListening(true);
|
||||||
startMessageListening();
|
startMessageListening();
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
|
|
||||||
}, [node, isWakuLoading, wakuStatus])
|
}, [node, isWakuLoading, wakuStatus])
|
||||||
|
|
||||||
const handleTelemetryOptIn = (optIn: boolean) => {
|
const handleTelemetryOptIn = (optIn: boolean) => {
|
||||||
@ -117,7 +112,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<Header wakuStatus={wakuStatus} />
|
<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>
|
<Routes>
|
||||||
<Route path="/create" element={<ChainCreationForm />} />
|
<Route path="/create" element={<ChainCreationForm />} />
|
||||||
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
|
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
|
||||||
@ -132,22 +127,20 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Home: React.FC = () => (
|
const Home: React.FC = () => (
|
||||||
<div className="space-y-6 text-center">
|
<div className="space-y-4 md:space-y-6 p-4 md:p-6">
|
||||||
<h1 className="text-4xl font-bold">BuddyChain</h1>
|
<h1 className="text-2xl md:text-4xl font-bold">BuddyBook</h1>
|
||||||
<div className="max-w-md mx-auto p-6 bg-card rounded-lg shadow-md">
|
<div className="w-full max-w-sm mx-auto p-4 md:p-6 bg-card rounded-lg shadow-md">
|
||||||
<Link to="/create">
|
<Link to="/create">
|
||||||
<Button
|
<Button className="w-full mb-4">
|
||||||
className="w-full mb-4"
|
|
||||||
>
|
|
||||||
Create New Chain
|
Create New Chain
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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.
|
Click the button above to start creating a new chain.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs md:text-sm text-muted-foreground text-center">
|
||||||
Welcome to BuddyChain - Create and share your chains!
|
Welcome to BuddyBook - Create and share your chains!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,10 +11,11 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
|
|
||||||
interface SignChainProps {
|
interface SignChainProps {
|
||||||
block: BlockPayload;
|
block: BlockPayload;
|
||||||
|
chainsData: BlockPayload[]; // Add this prop
|
||||||
onSuccess: (newBlock: BlockPayload) => void;
|
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 [isOpen, setIsOpen] = useState(false);
|
||||||
const [isSigning, setIsSigning] = useState(false);
|
const [isSigning, setIsSigning] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -25,17 +26,37 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (address) {
|
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);
|
setAlreadySigned(hasAlreadySigned);
|
||||||
}
|
}
|
||||||
}, [address, block.signatures]);
|
}, [address, block, chainsData]);
|
||||||
|
|
||||||
const { signMessage } = useSignMessage({
|
const { signMessage } = useSignMessage({
|
||||||
mutation: {
|
mutation: {
|
||||||
async onSuccess(signature) {
|
async onSuccess(signature) {
|
||||||
if (!address || !node) return;
|
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())) {
|
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
|
||||||
setError('You have already signed this chain.');
|
setError('You have already signed this chain.');
|
||||||
setIsSigning(false);
|
setIsSigning(false);
|
||||||
@ -79,6 +100,7 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSign = () => {
|
const handleSign = () => {
|
||||||
|
// Add an additional check here before signing
|
||||||
if (alreadySigned) {
|
if (alreadySigned) {
|
||||||
setError('You have already signed this chain.');
|
setError('You have already signed this chain.');
|
||||||
return;
|
return;
|
||||||
@ -102,7 +124,7 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
|
|||||||
{alreadySigned ? 'Already Signed' : 'Sign Chain'}
|
{alreadySigned ? 'Already Signed' : 'Sign Chain'}
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Sign Chain</DialogTitle>
|
<DialogTitle>Sign Chain</DialogTitle>
|
||||||
<DialogDescription>
|
<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.'}
|
: 'Review the block details and sign to add your signature to the chain.'}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</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>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
||||||
|
|||||||
@ -44,7 +44,11 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<h2 className="text-xl font-semibold mb-2">{block.title}</h2>
|
<h2 className="text-xl font-semibold mb-2">{block.title}</h2>
|
||||||
<p className="mb-4">{block.description}</p>
|
<p className="mb-4">{block.description}</p>
|
||||||
<SignChain block={block} onSuccess={onChainUpdate} />
|
<SignChain
|
||||||
|
block={block}
|
||||||
|
chainsData={chainsData}
|
||||||
|
onSuccess={onChainUpdate}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { type BlockPayload } from '@/lib/waku';
|
|||||||
import SignChain from '@/components/Chain/SignChain';
|
import SignChain from '@/components/Chain/SignChain';
|
||||||
import { useEnsName } from 'wagmi';
|
import { useEnsName } from 'wagmi';
|
||||||
import { Button } from '@/components/ui/button';
|
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 QRCode from '@/components/QRCode';
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
@ -50,7 +50,11 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
|
|||||||
Block UUID: {block.blockUUID}
|
Block UUID: {block.blockUUID}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 space-x-2">
|
<div className="mt-2 space-x-2">
|
||||||
<SignChain block={block} onSuccess={handleChainUpdate} />
|
<SignChain
|
||||||
|
block={block}
|
||||||
|
chainsData={chainsData}
|
||||||
|
onSuccess={handleChainUpdate}
|
||||||
|
/>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">Share</Button>
|
<Button variant="outline">Share</Button>
|
||||||
@ -58,6 +62,9 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
|
|||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Share this Chain</DialogTitle>
|
<DialogTitle>Share this Chain</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Share this chain with others to collect their signatures.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<QRCode text={shareUrl} width={200} height={200} />
|
<QRCode text={shareUrl} width={200} height={200} />
|
||||||
|
|||||||
@ -56,69 +56,78 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-background border-b border-border">
|
<header className="border-b">
|
||||||
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
|
<div className="container mx-auto px-4 py-2 md:py-4">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex flex-col md:flex-row justify-between items-center space-y-2 md:space-y-0">
|
||||||
<h1 className="text-2xl font-bold">BuddyBook</h1>
|
<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">
|
||||||
<nav>
|
<h1 className="text-xl md:text-2xl font-bold">BuddyBook</h1>
|
||||||
<ul className="flex space-x-4">
|
<nav className="w-full md:w-auto">
|
||||||
<li>
|
<ul className="flex justify-center md:justify-start space-x-4">
|
||||||
<Link
|
<li>
|
||||||
to="/create"
|
<Link
|
||||||
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
to="/create"
|
||||||
>
|
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
Create Chain
|
>
|
||||||
</Link>
|
Create Chain
|
||||||
</li>
|
</Link>
|
||||||
<li>
|
</li>
|
||||||
<Link
|
<li>
|
||||||
to="/view"
|
<Link
|
||||||
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
to="/view"
|
||||||
>
|
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
View Existing Chains
|
>
|
||||||
</Link>
|
View Chains
|
||||||
</li>
|
</Link>
|
||||||
<li>
|
</li>
|
||||||
<Link
|
<li>
|
||||||
to="/telemetry"
|
<Link
|
||||||
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
to="/telemetry"
|
||||||
>
|
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
|
||||||
Telemetry
|
>
|
||||||
</Link>
|
Telemetry
|
||||||
</li>
|
</Link>
|
||||||
</ul>
|
</li>
|
||||||
</nav>
|
</ul>
|
||||||
</div>
|
</nav>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span className="text-sm text-muted-foreground">Store:</span>
|
<div className="flex flex-wrap justify-center md:justify-end items-center gap-2 w-full md:w-auto">
|
||||||
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Check, Copy } from "lucide-react";
|
||||||
|
|
||||||
interface QRCodeProps {
|
interface QRCodeProps {
|
||||||
text: string;
|
text: string;
|
||||||
@ -8,9 +10,32 @@ interface QRCodeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
|
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 (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<QRCodeSVG value={text} size={Math.min(width, height)} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,41 +6,58 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { privacyPolicy } from '@/lib/privacyPolicy';
|
import { privacyPolicy } from '@/lib/privacyPolicy';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
interface TelemetryOptInProps {
|
interface PrivacyPolicyOptInProps {
|
||||||
onOptIn: (optIn: boolean) => void;
|
onOptIn: (optIn: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TelemetryOptIn: React.FC<TelemetryOptInProps> = ({ onOptIn }) => {
|
const PrivacyPolicyOptIn: React.FC<PrivacyPolicyOptInProps> = ({ onOptIn }) => {
|
||||||
const [showFullPolicy, setShowFullPolicy] = useState(false);
|
const [showFullPolicy, setShowFullPolicy] = useState(false);
|
||||||
|
|
||||||
return (
|
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">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader className="space-y-2">
|
||||||
<CardTitle>Telemetry Data Collection</CardTitle>
|
<CardTitle className="text-xl sm:text-2xl">Privacy Policy & Data Collection</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<div className="space-y-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 className="text-sm sm:text-base text-muted-foreground">
|
||||||
</p>
|
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.
|
||||||
<Button variant="link" onClick={() => setShowFullPolicy(true)}>
|
</p>
|
||||||
View Full Privacy Policy
|
<Button
|
||||||
</Button>
|
variant="link"
|
||||||
|
onClick={() => setShowFullPolicy(true)}
|
||||||
|
className="px-0 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
View Full Privacy Policy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between">
|
<CardFooter className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||||
<Button variant="outline" onClick={() => onOptIn(false)}>Opt Out</Button>
|
<Button
|
||||||
<Button onClick={() => onOptIn(true)}>Opt In</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>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Dialog open={showFullPolicy} onOpenChange={setShowFullPolicy}>
|
<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>
|
<DialogHeader>
|
||||||
<DialogTitle>Privacy Policy</DialogTitle>
|
<DialogTitle className="text-xl sm:text-2xl">Privacy Policy</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ScrollArea className="mt-4 h-[60vh]">
|
<ScrollArea className="mt-4 h-[50vh] sm:h-[60vh]">
|
||||||
<DialogDescription className="space-y-4">
|
<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}
|
{privacyPolicy}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@ -51,4 +68,4 @@ const TelemetryOptIn: React.FC<TelemetryOptInProps> = ({ onOptIn }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TelemetryOptIn;
|
export default PrivacyPolicyOptIn;
|
||||||
|
|||||||
@ -6,44 +6,49 @@ import { privacyPolicy } from '@/lib/privacyPolicy';
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
|
||||||
const TelemetryPage: React.FC = () => {
|
const PrivacyPolicyPage: React.FC = () => {
|
||||||
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean>(false);
|
const [privacyPolicyOptIn, setPrivacyPolicyOptIn] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedOptIn = localStorage.getItem('telemetryOptIn');
|
const storedOptIn = localStorage.getItem('privacyPolicyOptIn');
|
||||||
if (storedOptIn !== null) {
|
if (storedOptIn !== null) {
|
||||||
setTelemetryOptIn(storedOptIn === 'true');
|
setPrivacyPolicyOptIn(storedOptIn === 'true');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToggleTelemetry = () => {
|
const handleTogglePrivacyPolicy = () => {
|
||||||
const newOptIn = !telemetryOptIn;
|
const newOptIn = !privacyPolicyOptIn;
|
||||||
setTelemetryOptIn(newOptIn);
|
setPrivacyPolicyOptIn(newOptIn);
|
||||||
localStorage.setItem('telemetryOptIn', newOptIn.toString());
|
localStorage.setItem('privacyPolicyOptIn', newOptIn.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-4xl mx-auto">
|
<Card className="w-full max-w-4xl mx-auto p-4 sm:p-6">
|
||||||
<CardHeader>
|
<CardHeader className="space-y-2">
|
||||||
<CardTitle>Telemetry Settings</CardTitle>
|
<CardTitle className="text-2xl sm:text-3xl">Privacy Policy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 sm:space-y-8">
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
<p className="text-sm sm:text-base text-muted-foreground">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold mb-2">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
Current status: {telemetryOptIn ? 'Opted In' : 'Opted Out'}
|
<p className="font-semibold">
|
||||||
</p>
|
Current status: {privacyPolicyOptIn ? 'Opted In' : 'Opted Out'}
|
||||||
<Button onClick={handleToggleTelemetry}>
|
</p>
|
||||||
{telemetryOptIn ? 'Opt Out' : 'Opt In'}
|
<Button
|
||||||
</Button>
|
onClick={handleTogglePrivacyPolicy}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{privacyPolicyOptIn ? 'Opt Out' : 'Opt In'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4">Privacy Policy</h3>
|
<h3 className="text-lg sm:text-xl font-semibold mb-4">Privacy Policy</h3>
|
||||||
<ScrollArea className="h-[60vh] border rounded-md p-4">
|
<ScrollArea className="h-[50vh] sm:h-[60vh] border rounded-md p-2 sm:p-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}
|
{privacyPolicy}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@ -54,4 +59,4 @@ const TelemetryPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TelemetryPage;
|
export default PrivacyPolicyPage;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const Card = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -23,7 +23,7 @@ const CardHeader = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -56,11 +56,34 @@
|
|||||||
--chart-5: 340 75% 55%
|
--chart-5: 340 75% 55%
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export type BlockPayload = {
|
|||||||
parentBlockUUID: string | null;
|
parentBlockUUID: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentTopic = "/buddychain-dogfood/1/chain/proto";
|
const contentTopic = "/buddybook-dogfood/1/chain/proto";
|
||||||
|
|
||||||
export const encoder = createEncoder({
|
export const encoder = createEncoder({
|
||||||
contentTopic: contentTopic,
|
contentTopic: contentTopic,
|
||||||
|
|||||||
@ -12,4 +12,4 @@ export const config = createConfig(
|
|||||||
[mainnet.id]: http(),
|
[mainnet.id]: http(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
- [ x ] waku connections on header should have green/yellow/red color indicator
|
- [ x ] waku connections on header should have green/yellow/red color indicator
|
||||||
- [ ] clicking on the indicator should show a list of peers
|
- [ ] 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
|
- [ ] generate waku peer id using the wallet address
|
||||||
- [ ] telemetry
|
- [ ] telemetry
|
||||||
- [ x ] disclaimer
|
- [ x ] disclaimer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user