this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix percentage input UX

Hilke Ros 82e00d62 a829aa1d

+83 -19
+83 -19
components/SongForm.tsx
··· 71 71 const trimmed = value.trim(); 72 72 if (!trimmed) return undefined; 73 73 74 - const parsed = Number(trimmed); 74 + const normalized = trimmed.replace(",", "."); 75 + const parsed = Number(normalized); 75 76 if (Number.isNaN(parsed)) return undefined; 76 77 77 78 return Math.round(parsed * 100); 79 + } 80 + 81 + function getPercentageDrafts( 82 + parties: InterestedParty[], 83 + field: "performanceRoyaltiesPercentage" | "mechanicalRoyaltiesPercentage", 84 + ) { 85 + return parties.map((party) => formatPercentageForInput(party[field])); 78 86 } 79 87 80 88 interface SongToEdit { ··· 99 107 editingSong?.interestedParties 100 108 ? editingSong.interestedParties.map(() => emptyLookupState()) 101 109 : [emptyLookupState()] 110 + ); 111 + const [performanceDrafts, setPerformanceDrafts] = useState<string[]>(() => 112 + getPercentageDrafts( 113 + editingSong?.interestedParties && editingSong.interestedParties.length > 0 114 + ? editingSong.interestedParties 115 + : [{ name: "" }], 116 + "performanceRoyaltiesPercentage", 117 + ), 118 + ); 119 + const [mechanicalDrafts, setMechanicalDrafts] = useState<string[]>(() => 120 + getPercentageDrafts( 121 + editingSong?.interestedParties && editingSong.interestedParties.length > 0 122 + ? editingSong.interestedParties 123 + : [{ name: "" }], 124 + "mechanicalRoyaltiesPercentage", 125 + ), 102 126 ); 103 127 const lookupTimersRef = useRef<Record<number, ReturnType<typeof setTimeout>>>({}); 104 128 ··· 256 280 const addInterestedParty = () => { 257 281 setInterestedParties((prev) => [...prev, { name: "" }]); 258 282 setPartyLookups((prev) => [...prev, emptyLookupState()]); 283 + setPerformanceDrafts((prev) => [...prev, ""]); 284 + setMechanicalDrafts((prev) => [...prev, ""]); 259 285 }; 260 286 261 287 const removeInterestedParty = (index: number) => { ··· 272 298 273 299 setInterestedParties((prev) => prev.filter((_, i) => i !== index)); 274 300 setPartyLookups((prev) => prev.filter((_, i) => i !== index)); 301 + setPerformanceDrafts((prev) => prev.filter((_, i) => i !== index)); 302 + setMechanicalDrafts((prev) => prev.filter((_, i) => i !== index)); 303 + }; 304 + 305 + const onPercentageDraftChange = ( 306 + index: number, 307 + value: string, 308 + kind: "performance" | "mechanical", 309 + ) => { 310 + if (kind === "performance") { 311 + setPerformanceDrafts((prev) => { 312 + const updated = [...prev]; 313 + updated[index] = value; 314 + return updated; 315 + }); 316 + return; 317 + } 318 + 319 + setMechanicalDrafts((prev) => { 320 + const updated = [...prev]; 321 + updated[index] = value; 322 + return updated; 323 + }); 324 + }; 325 + 326 + const commitPercentageDraft = (index: number, kind: "performance" | "mechanical") => { 327 + const currentDraft = kind === "performance" ? performanceDrafts[index] : mechanicalDrafts[index]; 328 + const parsed = parsePercentageInput(currentDraft || ""); 329 + const normalized = formatPercentageForInput(parsed); 330 + 331 + if (kind === "performance") { 332 + updateInterestedParty(index, "performanceRoyaltiesPercentage", parsed); 333 + setPerformanceDrafts((prev) => { 334 + const updated = [...prev]; 335 + updated[index] = normalized; 336 + return updated; 337 + }); 338 + return; 339 + } 340 + 341 + updateInterestedParty(index, "mechanicalRoyaltiesPercentage", parsed); 342 + setMechanicalDrafts((prev) => { 343 + const updated = [...prev]; 344 + updated[index] = normalized; 345 + return updated; 346 + }); 275 347 }; 276 348 277 349 async function handleSubmit(e: React.FormEvent) { ··· 528 600 Performance % 529 601 </label> 530 602 <input 531 - type="number" 603 + type="text" 604 + inputMode="decimal" 532 605 step="0.01" 533 606 min="0" 534 - value={formatPercentageForInput(party.performanceRoyaltiesPercentage)} 535 - onChange={(e) => 536 - updateInterestedParty( 537 - index, 538 - "performanceRoyaltiesPercentage", 539 - parsePercentageInput(e.target.value), 540 - ) 541 - } 607 + value={performanceDrafts[index] ?? ""} 608 + onChange={(e) => onPercentageDraftChange(index, e.target.value, "performance")} 609 + onBlur={() => commitPercentageDraft(index, "performance")} 542 610 className="w-full px-2 py-1 text-sm border border-zinc-300 dark:border-zinc-700 rounded bg-white dark:bg-zinc-800" 543 611 disabled={loading} 544 612 placeholder="100.00" ··· 549 617 Mechanical % 550 618 </label> 551 619 <input 552 - type="number" 620 + type="text" 621 + inputMode="decimal" 553 622 step="0.01" 554 623 min="0" 555 - value={formatPercentageForInput(party.mechanicalRoyaltiesPercentage)} 556 - onChange={(e) => 557 - updateInterestedParty( 558 - index, 559 - "mechanicalRoyaltiesPercentage", 560 - parsePercentageInput(e.target.value), 561 - ) 562 - } 624 + value={mechanicalDrafts[index] ?? ""} 625 + onChange={(e) => onPercentageDraftChange(index, e.target.value, "mechanical")} 626 + onBlur={() => commitPercentageDraft(index, "mechanical")} 563 627 className="w-full px-2 py-1 text-sm border border-zinc-300 dark:border-zinc-700 rounded bg-white dark:bg-zinc-800" 564 628 disabled={loading} 565 629 placeholder="100.00"