Last active
January 8, 2026 15:45
-
-
Save masterfermin02/2e2065969349df0ec209aef067c35559 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from "react"; | |
| import { useForm } from "@inertiajs/react"; | |
| import MultiSelectChips from "@/Components/MultiSelectChips"; | |
| export default function Edit() { | |
| const techOptions = [ | |
| { value: "laravel", label: "Laravel" }, | |
| { value: "react", label: "React" }, | |
| { value: "dotnet", label: ".NET" }, | |
| { value: "twilio", label: "Twilio" }, | |
| { value: "postgres", label: "Postgres" }, | |
| ]; | |
| const { data, setData, post, processing, errors } = useForm({ | |
| technologies: ["react"], // initial values | |
| }); | |
| function submit(e) { | |
| e.preventDefault(); | |
| post(route("profile.update")); | |
| } | |
| return ( | |
| <form onSubmit={submit} className="space-y-6"> | |
| <div> | |
| <MultiSelectChips | |
| label="Technologies" | |
| name="technologies" | |
| options={techOptions} | |
| value={data.technologies} | |
| onChange={(next) => setData("technologies", next)} | |
| /> | |
| {errors.technologies && ( | |
| <p className="mt-2 text-sm text-red-600">{errors.technologies}</p> | |
| )} | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={processing} | |
| className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60" | |
| > | |
| Save | |
| </button> | |
| </form> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from "react"; | |
| export default function MultiSelectChips({ | |
| label, | |
| options, | |
| value, | |
| onChange, | |
| placeholder = "Select one or more…", | |
| disabled = false, | |
| name, | |
| }) { | |
| const [open, setOpen] = React.useState(false); | |
| const rootRef = React.useRef(null); | |
| // Close on outside click | |
| React.useEffect(() => { | |
| function onDocClick(e) { | |
| if (!rootRef.current) return; | |
| if (!rootRef.current.contains(e.target)) setOpen(false); | |
| } | |
| document.addEventListener("mousedown", onDocClick); | |
| return () => document.removeEventListener("mousedown", onDocClick); | |
| }, []); | |
| // Close on Esc | |
| React.useEffect(() => { | |
| function onKey(e) { | |
| if (e.key === "Escape") setOpen(false); | |
| } | |
| document.addEventListener("keydown", onKey); | |
| return () => document.removeEventListener("keydown", onKey); | |
| }, []); | |
| const selected = Array.isArray(value) ? value : []; | |
| const selectedOptions = options.filter((o) => selected.includes(o.value)); | |
| function toggleValue(v) { | |
| if (disabled) return; | |
| const next = selected.includes(v) | |
| ? selected.filter((x) => x !== v) | |
| : [...selected, v]; | |
| onChange(next); | |
| } | |
| function removeValue(v) { | |
| if (disabled) return; | |
| onChange(selected.filter((x) => x !== v)); | |
| } | |
| function clearAll() { | |
| if (disabled) return; | |
| onChange([]); | |
| } | |
| return ( | |
| <div className="w-full" ref={rootRef}> | |
| {label && ( | |
| <label className="mb-2 block text-sm font-medium text-gray-700"> | |
| {label} | |
| </label> | |
| )} | |
| {/* Hidden input(s) for classic form compatibility (optional) */} | |
| {name && | |
| selected.map((v) => ( | |
| <input key={v} type="hidden" name={`${name}[]`} value={v} /> | |
| ))} | |
| {/* Control */} | |
| <button | |
| type="button" | |
| disabled={disabled} | |
| onClick={() => setOpen((s) => !s)} | |
| className={[ | |
| "flex min-h-[44px] w-full items-center justify-between rounded-lg border bg-white px-3 py-2 text-left text-sm shadow-sm", | |
| "focus:outline-none focus:ring-2 focus:ring-blue-200", | |
| disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer", | |
| open ? "border-blue-500" : "border-gray-300", | |
| ].join(" ")} | |
| > | |
| <div className="flex flex-wrap gap-2"> | |
| {selectedOptions.length === 0 ? ( | |
| <span className="text-gray-400">{placeholder}</span> | |
| ) : ( | |
| selectedOptions.map((o) => ( | |
| <span | |
| key={o.value} | |
| className="inline-flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-800" | |
| > | |
| {o.label} | |
| <span | |
| role="button" | |
| tabIndex={0} | |
| aria-label={`Remove ${o.label}`} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| removeValue(o.value); | |
| }} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter" || e.key === " ") { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| removeValue(o.value); | |
| } | |
| }} | |
| className="inline-flex h-4 w-4 items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-700" | |
| > | |
| × | |
| </span> | |
| </span> | |
| )) | |
| )} | |
| </div> | |
| <span className="ml-3 select-none text-gray-500"> | |
| {open ? "▲" : "▼"} | |
| </span> | |
| </button> | |
| {/* Dropdown */} | |
| {open && !disabled && ( | |
| <div className="mt-2 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg"> | |
| <ul className="max-h-56 overflow-auto py-1 text-sm"> | |
| {options.map((o) => { | |
| const isSelected = selected.includes(o.value); | |
| return ( | |
| <li key={o.value}> | |
| <button | |
| type="button" | |
| onClick={() => toggleValue(o.value)} | |
| className={[ | |
| "flex w-full items-center justify-between px-3 py-2 text-left", | |
| "hover:bg-gray-50", | |
| isSelected ? "bg-blue-50" : "", | |
| ].join(" ")} | |
| > | |
| <span className="text-gray-800">{o.label}</span> | |
| <span | |
| className={[ | |
| "ml-3 inline-flex h-5 w-5 items-center justify-center rounded border text-xs", | |
| isSelected | |
| ? "border-blue-500 bg-blue-500 text-white" | |
| : "border-gray-300 bg-white text-transparent", | |
| ].join(" ")} | |
| > | |
| ✓ | |
| </span> | |
| </button> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| <div className="flex items-center justify-between border-t border-gray-100 px-3 py-2"> | |
| <button | |
| type="button" | |
| onClick={clearAll} | |
| className="text-xs font-medium text-gray-600 hover:text-gray-900" | |
| > | |
| Clear | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setOpen(false)} | |
| className="rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-800" | |
| > | |
| Done | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment