UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

15 lines 5.64 kB
{ "name": "metalens-components-metalensbadge", "type": "registry:component", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/metalens/components/MetaLensBadge.tsx", "type": "registry:component", "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { useMetaLensProvider } from \"../components/MetaLensProvider.js\";\nimport { useMetaLens } from \"../hooks/useMetaLens.js\";\nimport type { MetaLensBadgeProps } from \"../types/index.js\";\nimport { validateContext } from \"../utils/protocol.js\";\n\n/**\n * Floating badge component showing comment count\n *\n * Based on patterns from:\n * - MetaLens browser extension badge display\n * - /Users/satchmo/code/metalens.app/BIGBLOCKS_COMPONENT_PLAN.md\n *\n * @example\n * ```tsx\n * <MetaLensBadge\n * context=\"url\"\n * value=\"https://example.com\"\n * position=\"bottom-right\"\n * onClick={() => setShowComments(true)}\n * />\n * ```\n */\nexport function MetaLensBadge({\n\tcontext,\n\tvalue,\n\tposition = \"bottom-right\",\n\tonClick,\n\ttheme = \"auto\",\n\tclassName,\n}: MetaLensBadgeProps) {\n\tconst { getCommentCount, subscribeToComments } = useMetaLens();\n\tconst { config, eventSourceManager } = useMetaLensProvider();\n\tconst [count, setCount] = useState<number>(0);\n\tconst [loading, setLoading] = useState(true);\n\tconst mountedRef = useRef(true);\n\n\tuseEffect(() => {\n\t\tmountedRef.current = true;\n\n\t\t// Validate context\n\t\tconst validation = validateContext(context, value);\n\t\tif (!validation.valid) {\n\t\t\tconsole.error(\"Invalid MetaLensBadge context:\", validation.error);\n\t\t\tsetLoading(false);\n\t\t\treturn;\n\t\t}\n\n\t\t// Fetch initial count\n\t\tconst fetchCount = async () => {\n\t\t\ttry {\n\t\t\t\tconst initialCount = await getCommentCount(context, value);\n\t\t\t\tif (mountedRef.current) {\n\t\t\t\t\tsetCount(initialCount);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Failed to fetch comment count:\", error);\n\t\t\t} finally {\n\t\t\t\tif (mountedRef.current) {\n\t\t\t\t\tsetLoading(false);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tfetchCount();\n\n\t\t// Set up real-time updates if enabled\n\t\tlet unsubscribe: (() => void) | null = null;\n\n\t\tif (config.enableRealTime) {\n\t\t\t// Subscribe to new comments to update count\n\t\t\tunsubscribe = subscribeToComments(context, value, () => {\n\t\t\t\tif (mountedRef.current) {\n\t\t\t\t\tsetCount((prev) => prev + 1);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Alternative: Set up dedicated count stream\n\t\t\tconst key = `badge-count-${context}-${value}`;\n\t\t\tconst path = `/stream/count?context=${context}&value=${value}&app=metalens`;\n\n\t\t\teventSourceManager.connect(key, path, {\n\t\t\t\tcount: (event: MessageEvent) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst newCount = Number.parseInt(event.data);\n\t\t\t\t\t\tif (!Number.isNaN(newCount) && mountedRef.current) {\n\t\t\t\t\t\t\tsetCount(newCount);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(\"Failed to parse count event:\", error);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Cleanup\n\t\treturn () => {\n\t\t\tmountedRef.current = false;\n\t\t\tif (unsubscribe) {\n\t\t\t\tunsubscribe();\n\t\t\t}\n\t\t\tif (config.enableRealTime) {\n\t\t\t\teventSourceManager.disconnect(`badge-count-${context}-${value}`);\n\t\t\t}\n\t\t};\n\t}, [\n\t\tcontext,\n\t\tvalue,\n\t\tgetCommentCount,\n\t\tsubscribeToComments,\n\t\tconfig.enableRealTime,\n\t\teventSourceManager,\n\t]);\n\n\t// Position styles based on prop\n\tconst positionStyles: React.CSSProperties = {\n\t\tposition: \"fixed\",\n\t\tzIndex: 9999,\n\t\t...(position === \"top-right\" && { top: 20, right: 20 }),\n\t\t...(position === \"top-left\" && { top: 20, left: 20 }),\n\t\t...(position === \"bottom-right\" && { bottom: 20, right: 20 }),\n\t\t...(position === \"bottom-left\" && { bottom: 20, left: 20 }),\n\t};\n\n\t// Detect system theme for 'auto' mode\n\tconst getSystemTheme = () => {\n\t\tif (typeof window !== \"undefined\" && window.matchMedia) {\n\t\t\treturn window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n\t\t\t\t? \"dark\"\n\t\t\t\t: \"light\";\n\t\t}\n\t\treturn \"light\";\n\t};\n\n\tconst resolvedTheme = theme === \"auto\" ? getSystemTheme() : theme;\n\n\t// Remove theme styles, we'll use shadcn/ui Button theme\n\n\treturn (\n\t\t<Button\n\t\t\tvariant=\"secondary\"\n\t\t\tsize=\"lg\"\n\t\t\tclassName={cn(\"flex items-center gap-2 shadow-md\", className)}\n\t\t\tstyle={{\n\t\t\t\t...positionStyles,\n\t\t\t\tcursor: onClick ? \"pointer\" : \"default\",\n\t\t\t}}\n\t\t\tonClick={onClick}\n\t\t\tdisabled={loading}\n\t\t>\n\t\t\t<svg\n\t\t\t\twidth=\"16\"\n\t\t\t\theight=\"16\"\n\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"2\"\n\t\t\t>\n\t\t\t\t<title>Comments</title>\n\t\t\t\t<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\n\t\t\t</svg>\n\t\t\t<Badge\n\t\t\t\tvariant={count > 0 ? \"default\" : \"secondary\"}\n\t\t\t\tclassName=\"rounded-full\"\n\t\t\t>\n\t\t\t\t{loading ? \"...\" : count > 999 ? \"999+\" : count}\n\t\t\t</Badge>\n\t\t\t{!loading && count > 0 && (\n\t\t\t\t<span className=\"ml-2\">{count === 1 ? \"Comment\" : \"Comments\"}</span>\n\t\t\t)}\n\t\t</Button>\n\t);\n}\n", "target": "<%- config.aliases.components %>/metalensbadge.tsx" } ] }