bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 9.3 kB
JSON
{
"name": "inscriptions-components-rarityeditor",
"type": "registry:component",
"dependencies": [
"@radix-ui/react-icons"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/inscriptions/components/RarityEditor.tsx",
"type": "registry:component",
"content": "\"use client\";\n\n/**\n * RarityEditor Component\n *\n * Component for editing NFT collection rarity tiers with percentage and item count management\n * Stores values in localStorage for persistence across sessions\n */\n\nimport { MinusIcon, PlusIcon } from \"@radix-ui/react-icons\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { Badge } from \"../../ui/badge.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport type { CollectionRarity } from \"./CollectionMintButton.js\";\n\nexport interface RarityEditorProps {\n\t/** Storage key for localStorage persistence */\n\tstorageKey?: string;\n\t/** Total collection quantity (enables item count mode) */\n\ttotalQuantity?: number;\n\t/** Callback when rarities change */\n\tonChange?: (rarities: CollectionRarity[]) => void;\n\t/** Initial rarities if not using localStorage */\n\tinitialRarities?: CollectionRarity[];\n\t/** Whether to show validation badges */\n\tshowValidation?: boolean;\n}\n\n// Add id to CollectionRarity type for stable keys\ntype RarityWithId = CollectionRarity & { id: number };\n\nexport function RarityEditor({\n\tstorageKey = \"bigblocks-collection-rarities\",\n\ttotalQuantity,\n\tonChange,\n\tinitialRarities = [{ label: \"Common\", percentage: \"1.0000\", items: \"\" }],\n\tshowValidation = true,\n}: RarityEditorProps) {\n\tconst [rarities, setRarities] = useState<RarityWithId[]>(() =>\n\t\tinitialRarities.map((r, i) => ({ ...r, id: i + 1 })),\n\t);\n\tconst [nextId, setNextId] = useState(initialRarities.length + 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\tconst raritiesWithIds = parsed.map(\n\t\t\t\t\t\t(r: CollectionRarity, i: number) => ({\n\t\t\t\t\t\t\t...r,\n\t\t\t\t\t\t\tid: i + 1,\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tsetRarities(raritiesWithIds);\n\t\t\t\t\tsetNextId(raritiesWithIds.length + 1);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to parse stored rarities:\", 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 updateRarities = useCallback(\n\t\t(newRarities: RarityWithId[]) => {\n\t\t\tsetRarities(newRarities);\n\t\t\t// Strip id before calling onChange\n\t\t\tonChange?.(newRarities.map(({ id, ...rest }) => rest));\n\n\t\t\tif (storageKey && typeof window !== \"undefined\") {\n\t\t\t\tlocalStorage.setItem(\n\t\t\t\t\tstorageKey,\n\t\t\t\t\tJSON.stringify(newRarities.map(({ id, ...rest }) => rest)),\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\t[onChange, storageKey],\n\t);\n\n\t// Add new rarity tier\n\tconst addRarity = useCallback(() => {\n\t\tupdateRarities([\n\t\t\t...rarities,\n\t\t\t{ id: nextId, label: \"\", percentage: \"0\", items: \"\" },\n\t\t]);\n\t\tsetNextId(nextId + 1);\n\t}, [rarities, updateRarities, nextId]);\n\n\t// Remove rarity tier\n\tconst removeRarity = useCallback(\n\t\t(id: number) => {\n\t\t\tupdateRarities(rarities.filter((r) => r.id !== id));\n\t\t},\n\t\t[rarities, updateRarities],\n\t);\n\n\t// Update specific rarity field\n\tconst updateRarity = useCallback(\n\t\t(id: number, field: keyof CollectionRarity, value: string) => {\n\t\t\tconst newRarities = rarities.map((rarity) => {\n\t\t\t\tif (rarity.id !== id) return rarity;\n\n\t\t\t\tlet updatedRarity = { ...rarity };\n\n\t\t\t\tif (field === \"percentage\") {\n\t\t\t\t\tconst percentValue = Number.parseFloat(value || \"0\") / 100;\n\t\t\t\t\tupdatedRarity = {\n\t\t\t\t\t\t...updatedRarity,\n\t\t\t\t\t\tpercentage: percentValue.toFixed(4),\n\t\t\t\t\t};\n\t\t\t\t} else if (field === \"items\" && totalQuantity) {\n\t\t\t\t\tupdatedRarity = { ...updatedRarity, items: value };\n\t\t\t\t} else {\n\t\t\t\t\tupdatedRarity = { ...updatedRarity, [field]: value };\n\t\t\t\t}\n\t\t\t\treturn updatedRarity;\n\t\t\t});\n\n\t\t\t// Recalculate percentages if item counts change\n\t\t\tif (field === \"items\" && totalQuantity) {\n\t\t\t\tconst totalItems = newRarities.reduce(\n\t\t\t\t\t(sum, r) => sum + Number.parseInt(r.items || \"0\"),\n\t\t\t\t\t0,\n\t\t\t\t);\n\t\t\t\tif (totalItems > 0) {\n\t\t\t\t\tnewRarities.forEach((rarity, i) => {\n\t\t\t\t\t\tconst itemCount = Number.parseInt(rarity.items || \"0\");\n\t\t\t\t\t\tconst percentage = itemCount / totalItems;\n\t\t\t\t\t\tnewRarities[i] = { ...rarity, percentage: percentage.toFixed(4) };\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tupdateRarities(newRarities);\n\t\t},\n\t\t[rarities, totalQuantity, updateRarities],\n\t);\n\n\t// Calculate totals for validation\n\tconst totalPercentage = rarities.reduce(\n\t\t(sum, r) => sum + Number.parseFloat(r.percentage || \"0\"),\n\t\t0,\n\t);\n\tconst totalItems = rarities.reduce(\n\t\t(sum, r) => sum + Number.parseInt(r.items || \"0\"),\n\t\t0,\n\t);\n\n\tconst isPercentageValid = Math.abs(totalPercentage - 1) < 0.0001;\n\tconst isItemCountValid = !totalQuantity || totalItems === totalQuantity;\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\">Rarity Tiers</p>\n\t\t\t\t<Button size=\"default\" variant=\"secondary\" onClick={addRarity}>\n\t\t\t\t\t<PlusIcon className=\"mr-2 h-4 w-4\" />\n\t\t\t\t\tAdd Tier\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{rarities.map((rarity, index) => (\n\t\t\t\t\t<div key={rarity.id} className=\"flex gap-2 items-end\">\n\t\t\t\t\t\t<div className=\"flex flex-col gap-1 flex-1\">\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">Label</p>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder=\"e.g., Common\"\n\t\t\t\t\t\t\t\tvalue={rarity.label}\n\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\tupdateRarity(rarity.id, \"label\", e.target.value)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t<div className=\"flex flex-col gap-1 flex-1\">\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">Percentage</p>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder=\"50\"\n\t\t\t\t\t\t\t\tvalue={(\n\t\t\t\t\t\t\t\t\tNumber.parseFloat(rarity.percentage || \"0\") * 100\n\t\t\t\t\t\t\t\t).toFixed(2)}\n\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\tupdateRarity(rarity.id, \"percentage\", e.target.value)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\t\tmax=\"100\"\n\t\t\t\t\t\t\t\tstep=\"0.01\"\n\t\t\t\t\t\t\t\tdisabled={!!totalQuantity} // Disable if using item counts\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{totalQuantity && (\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-1 w-[120px]\">\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">Items</p>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tplaceholder=\"0\"\n\t\t\t\t\t\t\t\t\tvalue={rarity.items || \"\"}\n\t\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\t\tupdateRarity(rarity.id, \"items\", e.target.value)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\tmin=\"0\"\n\t\t\t\t\t\t\t\t\tmax={totalQuantity}\n\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\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tsize=\"default\"\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tclassName=\"text-destructive hover:bg-destructive hover:text-destructive-foreground p-0 h-9 w-9\"\n\t\t\t\t\t\t\tonClick={() => removeRarity(rarity.id)}\n\t\t\t\t\t\t\tdisabled={rarities.length === 1}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<MinusIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\n\t\t\t{showValidation && (\n\t\t\t\t<div className=\"flex items-center gap-3 justify-between\">\n\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t<p className=\"text-sm font-medium\">Total:</p>\n\t\t\t\t\t\t<Badge variant={isPercentageValid ? \"default\" : \"destructive\"}>\n\t\t\t\t\t\t\t{(totalPercentage * 100).toFixed(2)}%\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t{totalQuantity && (\n\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">Items:</p>\n\t\t\t\t\t\t\t<Badge variant={isItemCountValid ? \"default\" : \"destructive\"}>\n\t\t\t\t\t\t\t\t{totalItems} / {totalQuantity}\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t{totalQuantity\n\t\t\t\t\t? \"Enter the number of items for each rarity tier. Percentages will be calculated automatically.\"\n\t\t\t\t\t: \"Define rarity percentages that total 100%. These determine the distribution of traits in your collection.\"}\n\t\t\t</p>\n\t\t</div>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/rarityeditor.tsx"
}
]
}