chore(buddybook): improvements (#106)

This commit is contained in:
Danish Arora 2024-11-04 21:57:12 +05:30 committed by GitHub
parent 7faa598ca7
commit e5c9a06368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 201 additions and 176 deletions

View File

@ -71,13 +71,17 @@ function App() {
setIsLoadingChains(true);
const messageGenerator = getMessagesFromStore(node as LightNode);
// Process messages as they arrive
for await (const message of messageGenerator) {
setChainsData(prevChains => {
const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID);
if (blockExists) return prevChains;
return [...prevChains, message];
});
try {
for await (const message of messageGenerator) {
setChainsData(prevChains => {
const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID);
if (blockExists) return prevChains;
return [...prevChains, message];
});
}
} catch (error) {
console.error("Error processing message:", error);
// Continue processing other messages
}
setWakuStatus(prev => ({ ...prev, store: 'success' }));
@ -130,9 +134,9 @@ function App() {
<Header wakuStatus={wakuStatus} />
<main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl">
<Routes>
<Route path="" element={<Home />} />
<Route path="create" element={<ChainCreationForm />} />
<Route path="view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
<Route path="" element={<Home />} />
<Route
path="sign/:chainUUID/:blockUUID"
element={

View File

@ -145,6 +145,7 @@ const ChainCreationForm: React.FC = () => {
value={formData.title}
onChange={handleInputChange}
maxLength={50}
className="text-base sm:text-sm"
/>
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
</div>
@ -156,10 +157,11 @@ const ChainCreationForm: React.FC = () => {
value={formData.description}
onChange={handleInputChange}
maxLength={500}
className="min-h-[100px] text-base sm:text-sm"
/>
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
</div>
<Button type="submit" className="w-full">Create Chain</Button>
<Button type="submit" className="w-full py-6 text-base sm:py-2 sm:text-sm">Create Chain</Button>
</form>
</CardContent>
<Dialog open={showModal} onOpenChange={handleCloseModal}>
@ -196,15 +198,15 @@ const ChainCreationForm: React.FC = () => {
<>
<div className="flex flex-col items-center space-y-4">
<QRCode
text={`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`}
text={`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`}
width={200}
height={200}
/>
<p className="text-sm text-center break-all">
{`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`}
{`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`}
</p>
<Button
onClick={() => navigator.clipboard.writeText(`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`)}
onClick={() => navigator.clipboard.writeText(`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`)}
variant="outline"
>
Copy Link

View File

@ -55,28 +55,32 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
const { signMessage } = useSignMessage({
mutation: {
onMutate() {
// Reset any previous errors when starting a new signing attempt
setError(null);
setIsSigning(true);
},
async onSuccess(signature) {
if (!address || !node) return;
// Double check signature before proceeding
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.');
setIsSigning(false);
return;
}
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
try {
// Double check signature before proceeding
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.');
return;
}
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
const wakuMessage = createMessage(newBlock);
const { failures, successes } = await node.lightPush.send(encoder, wakuMessage);
@ -89,44 +93,55 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
} catch (error) {
console.error('Error creating new block:', error);
setError('Failed to create new block. Please try again.');
} finally {
setIsSigning(false);
}
},
onError(error) {
console.error('Error signing message:', error);
setError('Error signing message. Please try again.');
setError('Error signing message. Please try again. If using a mobile wallet, please ensure your wallet app is open.');
},
onSettled() {
setIsSigning(false);
}
}
});
const handleSign = () => {
if (!ensureWalletConnected()) {
return;
const handleSign = async () => {
try {
if (!address) {
// If not connected, try to connect first
const connected = await ensureWalletConnected();
if (!connected) return;
}
// Check if already signed
if (alreadySigned) {
setError('You have already signed this chain.');
return;
}
// Prepare the message
const message = `Sign Block:
Chain UUID: ${block.chainUUID}
Block UUID: ${block.blockUUID}
Title: ${block.title}
Description: ${block.description}
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID}
Signed by: ${ensName || address}`;
// Trigger signing
signMessage({ message });
} catch (error) {
console.error('Error in sign flow:', error);
setError('Failed to initiate signing. Please try again.');
setIsSigning(false);
}
// Add an additional check here before signing
if (alreadySigned) {
setError('You have already signed this chain.');
return;
}
setIsSigning(true);
setError(null);
const message = `Sign Block:
Chain UUID: ${block.chainUUID}
Block UUID: ${block.blockUUID}
Title: ${block.title}
Description: ${block.description}
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID}
Signed by: ${ensName || address}`;
signMessage({ message });
};
return (
<>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
{alreadySigned ? 'Already Signed' : 'Sign Chain'}
{alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Chain'}
</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-md">
@ -157,6 +172,8 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
</>
) : alreadySigned ? (
'Already Signed'
) : !address ? (
'Connect Wallet'
) : (
'Sign'
)}

View File

@ -23,7 +23,7 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
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}`;
const shareUrl = `${window.location.origin}${import.meta.env.BASE_URL}sign/${block.chainUUID ?? block.blockUUID}/${block.blockUUID}`;
return (
<li key={`${block.blockUUID}-${depth}`} className="mb-4">

View File

@ -56,90 +56,70 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
};
return (
<header className="border-b">
<div className="container mx-auto px-4 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.endsWith('/create') ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Create Chain
</Link>
</li>
<li>
<Link
to="view"
className={`text-sm ${location.pathname.endsWith('/view') ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
View Chains
</Link>
</li>
<li>
<Link
to="telemetry"
className={`text-sm ${location.pathname.endsWith('/telemetry') ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Telemetry
</Link>
</li>
</ul>
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container">
<div className="h-14">
<div className="flex h-14 items-center justify-between gap-4">
<nav className="flex items-center gap-2 md:gap-4">
<Link
to=""
className={`text-sm font-medium ${location.pathname === "" ? "text-foreground" : "text-muted-foreground"}`}
>
Home
</Link>
<Link
to="create"
className={`text-sm font-medium ${location.pathname === "/create" ? "text-foreground" : "text-muted-foreground"}`}
>
Create
</Link>
<Link
to="view"
className={`text-sm font-medium ${location.pathname === "/view" ? "text-foreground" : "text-muted-foreground"}`}
>
View
</Link>
</nav>
</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">
{isWakuLoading ? (
<div className="flex items-center space-x-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground">Connecting...</span>
</div>
) : wakuError ? (
<span className="text-destructive">Network Error</span>
) : (
<>
<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>
<span className="text-xs md:text-sm text-muted-foreground hidden md:inline">
{connections > 0 ? `${connections} peer${connections === 1 ? '' : 's'}` : 'Connecting...'}
</span>
</>
)}
</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>
)}
<div className="flex items-center gap-2 md:gap-4">
<div className="hidden md:flex items-center gap-2">
{!isWakuLoading && !wakuError && (
<>
<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>
{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 className="flex items-center gap-2">
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-xs text-red-500">Error</span>
) : (
<div className={`w-2 h-2 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
)}
{isConnected ? (
<div className="flex items-center gap-2">
<span className="text-xs md:text-sm text-muted-foreground truncate max-w-[80px] md:max-w-[120px]">
{ensName || (address ? `${address.slice(0, 4)}...${address.slice(-4)}` : '')}
</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
<span className="md:hidden">×</span>
<span className="hidden md:inline">Logout</span>
</Button>
</div>
) : (
<ConnectKitButton />
)}
</div>
</div>
</div>
</div>

View File

@ -11,6 +11,7 @@ interface QRCodeProps {
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
const [copied, setCopied] = useState(false);
const isMobile = window.innerWidth < 640;
const handleCopy = async () => {
await navigator.clipboard.writeText(text);
@ -20,18 +21,22 @@ const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
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">
<QRCodeSVG
value={text}
size={isMobile ? Math.min(width * 0.8, window.innerWidth - 64) : Math.min(width, height)}
/>
<div className="flex items-center space-x-2 w-full max-w-[300px]">
<input
type="text"
value={text}
readOnly
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted"
className="flex-1 px-3 py-2 text-xs sm:text-sm border rounded-md bg-muted truncate"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopy}
className="shrink-0"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>

View File

@ -45,7 +45,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"min-h-[44px] px-4 py-2 md:min-h-[36px] md:px-3 md:py-1.5",
buttonVariants({ variant, size, className })
)}
ref={ref}
{...props}
/>

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm p-4 md:p-6",
"rounded-lg border bg-card text-card-foreground shadow-sm p-4 md:p-6 w-full max-w-[95vw] mx-auto",
className
)}
{...props}

View File

@ -30,24 +30,16 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"max-h-[85vh] overflow-y-auto",
className
)}
{...props}
/>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

View File

@ -10,7 +10,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"touch-manipulation min-h-[44px] md:min-h-[36px]",
className
)}
ref={ref}

View File

@ -9,7 +9,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base md:text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"touch-manipulation resize-y",
className
)}
ref={ref}

View File

@ -4,14 +4,21 @@ export function useWalletPrompt() {
const { isConnected } = useAccount()
const { connect, connectors } = useConnect()
const ensureWalletConnected = () => {
const ensureWalletConnected = async () => {
if (!isConnected) {
// Find the first available connector (usually injected/metamask)
const connector = connectors[0]
if (connector) {
connect({ connector })
try {
// Find the first available connector (usually injected/metamask)
const connector = connectors[0]
if (connector) {
await connect({ connector })
}
// Wait a brief moment for the connection to be established
await new Promise(resolve => setTimeout(resolve, 1000));
return true
} catch (error) {
console.error('Error connecting wallet:', error)
return false
}
return false
}
return true
}

View File

@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
@ -27,8 +28,9 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
@ -53,36 +55,46 @@
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-tap-highlight-color: transparent;
}
body {
@apply bg-background text-foreground;
}
overscroll-behavior-y: none;
}
button, a {
@apply cursor-pointer touch-manipulation;
}
.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;
}