UNPKG

bigblocks

Version:

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

18 lines 9.57 kB
{ "name": "inscriptions-components-traiteditor", "type": "registry:component", "dependencies": [ "@radix-ui/react-icons", "js-1sat-ord" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "components/inscriptions/components/TraitEditor.tsx", "type": "registry:component", "content": "\"use client\";\n\n/**\n * TraitEditor Component\n *\n * Component for editing NFT collection traits with values and occurrence percentages\n * Stores values in localStorage for persistence across sessions\n */\n\nimport { Cross2Icon, PlusIcon } from \"@radix-ui/react-icons\";\nimport type { CollectionTraits } from \"js-1sat-ord\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Textarea } from \"../../ui/textarea.js\";\n\nexport interface TraitEditorProps {\n\t/** Storage key for localStorage persistence */\n\tstorageKey?: string;\n\t/** Total collection quantity (for percentage calculations) */\n\ttotalQuantity?: number;\n\t/** Callback when traits change */\n\tonChange?: (traits: CollectionTraits) => void;\n\t/** Initial traits if not using localStorage */\n\tinitialTraits?: CollectionTraits;\n}\n\nexport function TraitEditor({\n\tstorageKey = \"bigblocks-collection-traits\",\n\ttotalQuantity,\n\tonChange,\n\tinitialTraits = {},\n}: TraitEditorProps) {\n\tconst [traits, setTraits] = useState<CollectionTraits>(initialTraits);\n\tconst [traitCounter, setTraitCounter] = useState(1);\n\n\t// Load from localStorage on mount\n\tuseEffect(() => {\n\t\tif (storageKey && typeof window !== \"undefined\") {\n\t\t\tconst stored = localStorage.getItem(storageKey);\n\t\t\tif (stored) {\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(stored);\n\t\t\t\t\tsetTraits(parsed);\n\t\t\t\t\t// Set counter based on existing trait count\n\t\t\t\t\tsetTraitCounter(Object.keys(parsed).length + 1);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to parse stored traits:\", error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [storageKey]);\n\n\t// Save to localStorage and notify parent\n\tconst updateTraits = useCallback(\n\t\t(newTraits: CollectionTraits) => {\n\t\t\tsetTraits(newTraits);\n\t\t\tonChange?.(newTraits);\n\n\t\t\tif (storageKey && typeof window !== \"undefined\") {\n\t\t\t\tlocalStorage.setItem(storageKey, JSON.stringify(newTraits));\n\t\t\t}\n\t\t},\n\t\t[onChange, storageKey],\n\t);\n\n\t// Add new trait\n\tconst addTrait = useCallback(() => {\n\t\tconst traitName = `Trait ${traitCounter}`;\n\t\tupdateTraits({\n\t\t\t...traits,\n\t\t\t[traitName]: {\n\t\t\t\tvalues: [],\n\t\t\t\toccurancePercentages: [],\n\t\t\t},\n\t\t});\n\t\tsetTraitCounter(traitCounter + 1);\n\t}, [traits, traitCounter, updateTraits]);\n\n\t// Remove trait\n\tconst removeTrait = useCallback(\n\t\t(traitKey: string) => {\n\t\t\tconst newTraits = { ...traits };\n\t\t\tdelete newTraits[traitKey];\n\t\t\tupdateTraits(newTraits);\n\t\t},\n\t\t[traits, updateTraits],\n\t);\n\n\t// Update trait name\n\tconst updateTraitName = useCallback(\n\t\t(oldKey: string, newName: string) => {\n\t\t\tif (newName === oldKey || !newName) return;\n\n\t\t\tconst newTraits = { ...traits };\n\t\t\tconst trait = newTraits[oldKey];\n\t\t\tif (trait) {\n\t\t\t\tdelete newTraits[oldKey];\n\t\t\t\tnewTraits[newName] = trait;\n\t\t\t\tupdateTraits(newTraits);\n\t\t\t}\n\t\t},\n\t\t[traits, updateTraits],\n\t);\n\n\t// Update trait values\n\tconst updateTraitValues = useCallback(\n\t\t(traitKey: string, valuesText: string) => {\n\t\t\tconst values = valuesText\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((v) => v.trim())\n\t\t\t\t.filter((v) => v);\n\n\t\t\tconst trait = traits[traitKey];\n\t\t\tif (!trait) return;\n\n\t\t\t// Auto-calculate equal percentages for each value\n\t\t\tconst percentages =\n\t\t\t\tvalues.length > 0\n\t\t\t\t\t? values.map(() => (1 / values.length).toFixed(4))\n\t\t\t\t\t: [];\n\n\t\t\tupdateTraits({\n\t\t\t\t...traits,\n\t\t\t\t[traitKey]: {\n\t\t\t\t\t...trait,\n\t\t\t\t\tvalues,\n\t\t\t\t\toccurancePercentages: percentages,\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\t[traits, updateTraits],\n\t);\n\n\t// Update trait percentages\n\tconst updateTraitPercentages = useCallback(\n\t\t(traitKey: string, percentagesText: string) => {\n\t\t\tconst trait = traits[traitKey];\n\t\t\tif (!trait) return;\n\n\t\t\tconst percentages = percentagesText\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((p) => p.trim())\n\t\t\t\t.filter((p) => p)\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst value = Number.parseFloat(p) / 100;\n\t\t\t\t\treturn value.toFixed(4);\n\t\t\t\t});\n\n\t\t\t// Ensure we have the right number of percentages\n\t\t\tif (!trait || percentages.length !== trait.values.length) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Ensure percentages sum to 1\n\t\t\tconst sum = percentages.reduce((acc, p) => acc + Number.parseFloat(p), 0);\n\t\t\tif (Math.abs(sum - 1) > 0.0001) {\n\t\t\t\t// Adjust the last percentage to make it sum to 1\n\t\t\t\tconst adjustment = 1 - sum;\n\t\t\t\tconst lastIndex = percentages.length - 1;\n\t\t\t\tif (lastIndex >= 0 && percentages[lastIndex] !== undefined) {\n\t\t\t\t\tpercentages[lastIndex] = (\n\t\t\t\t\t\tNumber.parseFloat(percentages[lastIndex]) + adjustment\n\t\t\t\t\t).toFixed(4);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tupdateTraits({\n\t\t\t\t...traits,\n\t\t\t\t[traitKey]: {\n\t\t\t\t\t...trait,\n\t\t\t\t\toccurancePercentages: percentages,\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\t[traits, updateTraits],\n\t);\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t<p className=\"text-base font-medium\">Trait Categories</p>\n\t\t\t\t<Button size=\"default\" variant=\"secondary\" onClick={addTrait}>\n\t\t\t\t\t<PlusIcon className=\"mr-2 h-4 w-4\" />\n\t\t\t\t\tAdd Trait\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t{Object.entries(traits).map(([traitKey, trait]) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={traitKey}\n\t\t\t\t\t\tclassName=\"flex flex-col gap-3 p-3 border rounded-lg bg-muted\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"flex justify-between items-center\">\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder=\"Trait name\"\n\t\t\t\t\t\t\t\tvalue={traitKey}\n\t\t\t\t\t\t\t\tonChange={(e) => updateTraitName(traitKey, e.target.value)}\n\t\t\t\t\t\t\t\tclassName=\"max-w-[200px]\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tclassName=\"text-destructive hover:bg-destructive hover:text-destructive-foreground h-8 w-8 p-0\"\n\t\t\t\t\t\t\t\tonClick={() => removeTrait(traitKey)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Cross2Icon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\tValues (comma-separated)\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\tplaceholder=\"e.g., Red, Blue, Green, Yellow\"\n\t\t\t\t\t\t\t\tvalue={trait.values.join(\", \")}\n\t\t\t\t\t\t\t\tonChange={(e) => updateTraitValues(traitKey, e.target.value)}\n\t\t\t\t\t\t\t\tclassName=\"min-h-[60px]\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t{trait.values.length} values defined\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{trait.values.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tOccurrence Percentages (comma-separated, must total 100%)\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tplaceholder=\"e.g., 25, 25, 25, 25\"\n\t\t\t\t\t\t\t\t\tvalue={\n\t\t\t\t\t\t\t\t\t\ttrait.occurancePercentages\n\t\t\t\t\t\t\t\t\t\t\t?.map((p) =>\n\t\t\t\t\t\t\t\t\t\t\t\t(Number.parseFloat(p || \"0\") * 100).toFixed(2),\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t.join(\", \") || \"\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\t\tupdateTraitPercentages(traitKey, e.target.value)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdisabled={totalQuantity !== undefined} // Auto-calculated when quantity is set\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{trait.occurancePercentages &&\n\t\t\t\t\t\t\t\t\ttrait.occurancePercentages.length > 0 && (\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\tTotal:{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t{(\n\t\t\t\t\t\t\t\t\t\t\t\ttrait.occurancePercentages.reduce(\n\t\t\t\t\t\t\t\t\t\t\t\t\t(sum, p) => sum + Number.parseFloat(p || \"0\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t\t\t\t\t) * 100\n\t\t\t\t\t\t\t\t\t\t\t).toFixed(2)}\n\t\t\t\t\t\t\t\t\t\t\t%\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\n\t\t\t{Object.keys(traits).length === 0 && (\n\t\t\t\t<p className=\"text-sm text-muted-foreground text-center p-4\">\n\t\t\t\t\tNo traits defined yet. Add trait categories to define the attributes\n\t\t\t\t\tof your collection items.\n\t\t\t\t</p>\n\t\t\t)}\n\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\tTraits define the variable attributes of items in your collection. Each\n\t\t\t\ttrait can have multiple values with different occurrence rates.\n\t\t\t</p>\n\t\t</div>\n\t);\n}\n", "target": "<%- config.aliases.components %>/traiteditor.tsx" } ] }