mirror of
https://github.com/waku-org/js-waku-lab.git
synced 2025-02-10 16:06:38 +00:00
feat: sharable view links
This commit is contained in:
parent
526e307af1
commit
2753453595
@ -7,10 +7,11 @@ import { Button } from "@/components/ui/button"
|
||||
import { type LightNode } from "@waku/sdk"
|
||||
import { useWaku } from "@waku/react"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { Routes, Route, Navigate, Link } from 'react-router-dom'
|
||||
import { Routes, Route, Navigate, Link, useParams } from 'react-router-dom'
|
||||
import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/waku'
|
||||
import TelemetryOptIn from './components/TelemetryOptIn';
|
||||
import TelemetryPage from './components/TelemetryPage';
|
||||
import SignSharedChain from './components/Chain/SignSharedChain'
|
||||
|
||||
type Status = 'success' | 'in-progress' | 'error';
|
||||
|
||||
@ -29,6 +30,7 @@ function App() {
|
||||
store: 'in-progress',
|
||||
});
|
||||
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
|
||||
const [isLoadingChains, setIsLoadingChains] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const storedOptIn = localStorage.getItem('telemetryOptIn');
|
||||
@ -65,12 +67,15 @@ function App() {
|
||||
console.log("Starting message listening")
|
||||
try {
|
||||
setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
|
||||
setIsLoadingChains(true);
|
||||
const storeMessages = await getMessagesFromStore(node as LightNode)
|
||||
setChainsData(storeMessages)
|
||||
setWakuStatus(prev => ({ ...prev, store: 'success' }));
|
||||
} catch (error) {
|
||||
console.error("Error fetching messages from store:", error);
|
||||
setWakuStatus(prev => ({ ...prev, store: 'error' }));
|
||||
} finally {
|
||||
setIsLoadingChains(false);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -115,10 +120,11 @@ function App() {
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<Routes>
|
||||
<Route path="/create" element={<ChainCreationForm />} />
|
||||
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} />} />
|
||||
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
<Route path="/sign/:chainUUID/:blockUUID" element={<SignSharedChain chainsData={chainsData} onChainUpdate={handleChainUpdate} />} />
|
||||
<Route path="/telemetry" element={<TelemetryPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
53
examples/buddybook/src/components/Chain/SignSharedChain.tsx
Normal file
53
examples/buddybook/src/components/Chain/SignSharedChain.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { BlockPayload } from '@/lib/waku';
|
||||
import SignChain from './SignChain';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface SignSharedChainProps {
|
||||
chainsData: BlockPayload[];
|
||||
onChainUpdate: (newBlock: BlockPayload) => void;
|
||||
}
|
||||
|
||||
const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUpdate }) => {
|
||||
const { chainUUID, blockUUID } = useParams();
|
||||
const [block, setBlock] = useState<BlockPayload | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const foundBlock = chainsData.find(b => b.chainUUID === chainUUID && b.blockUUID === blockUUID);
|
||||
if (foundBlock) {
|
||||
setBlock(foundBlock);
|
||||
}
|
||||
}, [chainsData, chainUUID, blockUUID]);
|
||||
|
||||
if (!block) {
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Chain Not Found</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4">The requested chain or block could not be found.</p>
|
||||
<Button onClick={() => navigate('/view')}>View All Chains</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign Shared Chain</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<h2 className="text-xl font-semibold mb-2">{block.title}</h2>
|
||||
<p className="mb-4">{block.description}</p>
|
||||
<SignChain block={block} onSuccess={onChainUpdate} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignSharedChain;
|
@ -1,15 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardHeader, CardTitle, CardContent} from "@/components/ui/card";
|
||||
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 QRCode from '@/components/QRCode';
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface ChainListProps {
|
||||
chainsData: BlockPayload[];
|
||||
onChainUpdate: (newBlock: BlockPayload) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
|
||||
const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoading }) => {
|
||||
const handleChainUpdate = (newBlock: BlockPayload) => {
|
||||
onChainUpdate(newBlock);
|
||||
};
|
||||
@ -18,6 +23,8 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
|
||||
const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID);
|
||||
const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0);
|
||||
|
||||
const shareUrl = `${window.location.origin}/sign/${block.chainUUID ?? block.blockUUID}/${block.blockUUID}`;
|
||||
|
||||
return (
|
||||
<li key={`${block.blockUUID}-${depth}`} className="mb-4">
|
||||
<div className="flex items-start">
|
||||
@ -42,8 +49,28 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Block UUID: {block.blockUUID}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 space-x-2">
|
||||
<SignChain block={block} onSuccess={handleChainUpdate} />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Share</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share this Chain</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<QRCode text={shareUrl} width={200} height={200} />
|
||||
<p className="text-sm text-center break-all">{shareUrl}</p>
|
||||
<Button
|
||||
onClick={() => navigator.clipboard.writeText(shareUrl)}
|
||||
variant="outline"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -70,7 +97,11 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
|
||||
<CardTitle>Existing Chains</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{rootBlocks.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-32">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : rootBlocks.length === 0 ? (
|
||||
<p>No chains found.</p>
|
||||
) : (
|
||||
<ul className="space-y-4">
|
||||
|
@ -1,53 +1,18 @@
|
||||
import React from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { BlockPayload } from '@/lib/waku';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEnsName } from 'wagmi';
|
||||
|
||||
interface QRCodeProps {
|
||||
data: BlockPayload;
|
||||
size?: number;
|
||||
onSign?: () => void;
|
||||
text: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const QRCode: React.FC<QRCodeProps> = ({ data, size = 256, onSign }) => {
|
||||
const shareableLink = `${window.location.origin}/view/${data.chainUUID}/${data.blockUUID}`;
|
||||
|
||||
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<QRCodeSVG value={shareableLink} size={size} />
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p><strong>Title:</strong> {data.title}</p>
|
||||
<p><strong>Description:</strong> {data.description}</p>
|
||||
<p><strong>Timestamp:</strong> {new Date(data.timestamp).toLocaleString()}</p>
|
||||
<p><strong>Signed Message:</strong> {`0x${data.signedMessage.slice(2, 6)}....${data.signedMessage.slice(-6)}`}</p>
|
||||
<p><strong>Parent Block:</strong> {data.parentBlockUUID || 'Root'}</p>
|
||||
<p><strong>Signatures:</strong></p>
|
||||
<ul>
|
||||
{data.signatures.map((sig, index) => (
|
||||
<SignatureItem key={index} address={sig.address} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={shareableLink}
|
||||
readOnly
|
||||
className="w-full p-2 border rounded"
|
||||
/>
|
||||
{onSign && <Button onClick={onSign}>Sign This Block</Button>}
|
||||
<QRCodeSVG value={text} size={Math.min(width, height)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SignatureItem: React.FC<{ address: `0x${string}` }> = ({ address }) => {
|
||||
const { data: ensName } = useEnsName({ address });
|
||||
|
||||
return (
|
||||
<li>
|
||||
{ensName || `${address.slice(0, 6)}...${address.slice(-4)}`}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRCode;
|
||||
|
Loading…
x
Reference in New Issue
Block a user