bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
18 lines • 11.5 kB
JSON
{
"name": "inscriptions-components-textinscriptionbutton",
"type": "registry:component",
"dependencies": [
"@radix-ui/react-icons",
"js-1sat-ord"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/inscriptions/components/TextInscriptionButton.tsx",
"type": "registry:component",
"content": "/**\n * TextInscriptionButton Component\n *\n * Component for creating text inscriptions with custom content types\n * Supports plain text, markdown, JSON, and other text formats\n */\n\nimport { FileTextIcon, InfoCircledIcon } from \"@radix-ui/react-icons\";\nimport type { PreMAP } from \"js-1sat-ord\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Input } from \"../../ui/input.js\";\nimport {\n\tSelect,\n\tSelectContent,\n\tSelectItem,\n\tSelectTrigger,\n\tSelectValue,\n} from \"../../ui/select.js\";\nimport { Switch } from \"../../ui/switch.js\";\nimport { Textarea } from \"../../ui/textarea.js\";\nimport { InscriptionButton } from \"./InscriptionButton.js\";\n\nconst COMMON_CONTENT_TYPES = [\n\t{ value: \"text/plain\", label: \"Plain Text\" },\n\t{ value: \"text/markdown\", label: \"Markdown\" },\n\t{ value: \"application/json\", label: \"JSON\" },\n\t{ value: \"text/css\", label: \"CSS\" },\n\t{ value: \"application/javascript\", label: \"JavaScript\" },\n\t{ value: \"text/csv\", label: \"CSV\" },\n\t{ value: \"text/yaml\", label: \"YAML\" },\n\t{ value: \"custom\", label: \"Custom...\" },\n];\n\nexport interface TextInscriptionButtonProps {\n\t/** Initial text content */\n\tinitialText?: string;\n\t/** Initial content type */\n\tinitialContentType?: string;\n\t/** Button text */\n\tbuttonText?: string;\n\t/** Dialog title */\n\tdialogTitle?: string;\n\t/** Button variant */\n\tvariant?: \"default\" | \"secondary\" | \"outline\" | \"ghost\";\n\t/** Button size */\n\tsize?: \"sm\" | \"default\" | \"lg\";\n\t/** Button color */\n\tcolor?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/** Whether button is disabled */\n\tdisabled?: boolean;\n\t/** Show advanced options by default */\n\tshowAdvancedByDefault?: boolean;\n\t/** Show fee estimate */\n\tshowFeeEstimate?: boolean;\n\t/** Max text length */\n\tmaxLength?: number;\n\t/** Success callback */\n\tonSuccess?: (txid: string, text: string, contentType: string) => void;\n\t/** Error callback */\n\tonError?: (error: Error) => void;\n}\n\nexport function TextInscriptionButton({\n\tinitialText = \"\",\n\tinitialContentType = \"text/plain\",\n\tbuttonText = \"Inscribe Text\",\n\tdialogTitle = \"Create Text Inscription\",\n\tvariant = \"default\",\n\tsize = \"sm\",\n\tcolor = \"blue\",\n\tclassName = \"\",\n\tdisabled = false,\n\tshowAdvancedByDefault = false,\n\tshowFeeEstimate = true,\n\tmaxLength = 100000, // ~100KB\n\tonSuccess,\n\tonError,\n}: TextInscriptionButtonProps) {\n\tconst [text, setText] = useState(initialText);\n\tconst [contentType, setContentType] = useState(initialContentType);\n\tconst [customContentType, setCustomContentType] = useState(\"\");\n\tconst [showAdvanced, setShowAdvanced] = useState(showAdvancedByDefault);\n\tconst [addMetadata, setAddMetadata] = useState(false);\n\tconst [metadataName, setMetadataName] = useState(\"\");\n\tconst [metadataDescription, setMetadataDescription] = useState(\"\");\n\n\t// Validation\n\tconst [errors, setErrors] = useState<Record<string, string>>({});\n\n\t// Get actual content type\n\tconst actualContentType = useMemo(() => {\n\t\treturn contentType === \"custom\" ? customContentType : contentType;\n\t}, [contentType, customContentType]);\n\n\t// Generate metadata\n\tconst generateMetadata = useCallback((): PreMAP | undefined => {\n\t\tif (!addMetadata) return undefined;\n\n\t\tconst metadata: PreMAP = {\n\t\t\tapp: \"bigblocks\",\n\t\t\ttype: \"inscription\",\n\t\t\tsubType: \"text\",\n\t\t\tcontentType: actualContentType,\n\t\t};\n\n\t\tif (metadataName) {\n\t\t\tmetadata.name = metadataName;\n\t\t}\n\n\t\tif (metadataDescription) {\n\t\t\tmetadata.description = metadataDescription;\n\t\t}\n\n\t\treturn metadata;\n\t}, [addMetadata, actualContentType, metadataName, metadataDescription]);\n\n\t// Validate form\n\tconst validateForm = useCallback((): boolean => {\n\t\tconst newErrors: Record<string, string> = {};\n\n\t\tif (!text || text.trim().length === 0) {\n\t\t\tnewErrors.text = \"Text content is required\";\n\t\t} else if (text.length > maxLength) {\n\t\t\tnewErrors.text = `Text exceeds maximum length of ${maxLength} characters`;\n\t\t}\n\n\t\tif (contentType === \"custom\" && !customContentType) {\n\t\t\tnewErrors.contentType = \"Custom content type is required\";\n\t\t}\n\n\t\tif (contentType === \"application/json\") {\n\t\t\ttry {\n\t\t\t\tJSON.parse(text);\n\t\t\t} catch {\n\t\t\t\tnewErrors.text = \"Invalid JSON format\";\n\t\t\t}\n\t\t}\n\n\t\tsetErrors(newErrors);\n\t\treturn Object.keys(newErrors).length === 0;\n\t}, [text, contentType, customContentType, maxLength]);\n\n\t// Handle success\n\tconst handleSuccess = useCallback(\n\t\t(txid: string) => {\n\t\t\tonSuccess?.(txid, text, actualContentType);\n\t\t},\n\t\t[text, actualContentType, onSuccess],\n\t);\n\n\t// Format text preview\n\tconst textPreview = useMemo(() => {\n\t\tif (!text) return \"No content\";\n\t\tconst lines = text.split(\"\\n\");\n\t\tconst preview = lines.slice(0, 3).join(\"\\n\");\n\t\tconst hasMore = lines.length > 3 || preview.length < text.length;\n\t\treturn preview + (hasMore ? \"\\n...\" : \"\");\n\t}, [text]);\n\n\t// Calculate text size\n\tconst textSize = useMemo(() => {\n\t\tconst bytes = new Blob([text]).size;\n\t\tif (bytes < 1024) return `${bytes} bytes`;\n\t\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\t\treturn `${(bytes / 1024 / 1024).toFixed(2)} MB`;\n\t}, [text]);\n\n\treturn (\n\t\t<InscriptionButton\n\t\t\tbuttonText={buttonText}\n\t\t\tdialogTitle={dialogTitle}\n\t\t\tvariant={\n\t\t\t\tvariant === \"default\"\n\t\t\t\t\t? \"solid\"\n\t\t\t\t\t: variant === \"secondary\"\n\t\t\t\t\t\t? \"soft\"\n\t\t\t\t\t\t: variant\n\t\t\t}\n\t\t\tsize={size === \"sm\" ? \"1\" : size === \"default\" ? \"2\" : \"3\"}\n\t\t\tclassName={className}\n\t\t\tdisabled={disabled}\n\t\t\ticon={<FileTextIcon />}\n\t\t\tcontent={validateForm() ? text : undefined}\n\t\t\tcontentType={validateForm() ? actualContentType : undefined}\n\t\t\tmetadata={generateMetadata()}\n\t\t\tshowFeeEstimate={showFeeEstimate}\n\t\t\tonSuccess={handleSuccess}\n\t\t\tonError={onError}\n\t\t>\n\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t{/* Text input */}\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t<span className=\"text-sm font-medium\">Text Content</span>\n\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\tvariant={text.length > maxLength ? \"destructive\" : \"secondary\"}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{text.length.toLocaleString()} / {maxLength.toLocaleString()}{\" \"}\n\t\t\t\t\t\t\tchars\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Textarea\n\t\t\t\t\t\tplaceholder=\"Enter your text content...\"\n\t\t\t\t\t\tvalue={text}\n\t\t\t\t\t\tonChange={(e) => setText(e.target.value)}\n\t\t\t\t\t\tstyle={{ minHeight: \"200px\", fontFamily: \"monospace\" }}\n\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t/>\n\t\t\t\t\t{errors.text && (\n\t\t\t\t\t\t<span className=\"text-xs text-destructive\">{errors.text}</span>\n\t\t\t\t\t)}\n\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">{textSize}</span>\n\t\t\t\t\t\t{text && (\n\t\t\t\t\t\t\t<Badge variant=\"secondary\">{text.split(\"\\n\").length} lines</Badge>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{/* Content type */}\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<span className=\"text-sm font-medium\">Content Type</span>\n\t\t\t\t\t<Select value={contentType} onValueChange={setContentType}>\n\t\t\t\t\t\t<SelectTrigger>\n\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t{COMMON_CONTENT_TYPES.map((type) => (\n\t\t\t\t\t\t\t\t<SelectItem key={type.value} value={type.value}>\n\t\t\t\t\t\t\t\t\t{type.label}\n\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t</Select>\n\n\t\t\t\t\t{contentType === \"custom\" && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder=\"e.g., text/x-python\"\n\t\t\t\t\t\t\t\tvalue={customContentType}\n\t\t\t\t\t\t\t\tonChange={(e) => setCustomContentType(e.target.value)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{errors.contentType && (\n\t\t\t\t\t\t\t\t<span className=\"text-xs text-destructive\">\n\t\t\t\t\t\t\t\t\t{errors.contentType}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\n\t\t\t\t{/* Advanced options toggle */}\n\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t<Switch checked={showAdvanced} onCheckedChange={setShowAdvanced} />\n\t\t\t\t\t<span className=\"text-sm\">Advanced Options</span>\n\t\t\t\t</div>\n\n\t\t\t\t{/* Advanced options */}\n\t\t\t\t{showAdvanced && (\n\t\t\t\t\t<div className=\"flex flex-col gap-3 p-3 bg-muted/50 rounded-md border\">\n\t\t\t\t\t\t{/* Metadata toggle */}\n\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t<Switch checked={addMetadata} onCheckedChange={setAddMetadata} />\n\t\t\t\t\t\t\t<span className=\"text-sm\">Add Metadata</span>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{addMetadata && (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tplaceholder=\"Name (optional)\"\n\t\t\t\t\t\t\t\t\tvalue={metadataName}\n\t\t\t\t\t\t\t\t\tonChange={(e) => setMetadataName(e.target.value)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\tplaceholder=\"Description (optional)\"\n\t\t\t\t\t\t\t\t\tvalue={metadataDescription}\n\t\t\t\t\t\t\t\t\tonChange={(e) => setMetadataDescription(e.target.value)}\n\t\t\t\t\t\t\t\t\tclassName=\"min-h-[60px]\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* Preview */}\n\t\t\t\t{text && (\n\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<span className=\"text-sm font-medium\">Preview</span>\n\t\t\t\t\t\t<div className=\"p-3 bg-muted/50 rounded-md border font-mono text-xs whitespace-pre-wrap break-words\">\n\t\t\t\t\t\t\t{textPreview}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* Info */}\n\t\t\t\t<div className=\"flex items-start gap-2 p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800\">\n\t\t\t\t\t<InfoCircledIcon className=\"mt-0.5 flex-shrink-0\" />\n\t\t\t\t\t<div className=\"flex flex-col gap-1\">\n\t\t\t\t\t\t<span className=\"text-sm font-medium\">Text Inscription</span>\n\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\tYour text will be permanently stored on the blockchain with the\n\t\t\t\t\t\t\tspecified content type.\n\t\t\t\t\t\t\t{contentType === \"text/markdown\" &&\n\t\t\t\t\t\t\t\t\" Markdown formatting will be preserved.\"}\n\t\t\t\t\t\t\t{contentType === \"application/json\" &&\n\t\t\t\t\t\t\t\t\" JSON will be validated before inscription.\"}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</InscriptionButton>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/textinscriptionbutton.tsx"
}
]
}