Opus
HomeGuideComponentsPlaygroundTheme designerSecurityVersion
opus-react v0.20.3
Resize panes

Source

Edit the component, then preview updates on the right.

Resize panes

ChatGPT

Add your OpenAI API key to chat. Conversations use the current source as context.

Your key stays on this device and is only used for your ChatGPT requests.

ChatGPT

Enter your key in the OpenAI API key field above, then ask about this source, props, layout, or how to reshape the example. Your key stays on this device.

Preview

Live render of your edited component.

Code changes
1 "use client";2  3+import { useMemo, useState } from "react";4+import { Button, Card, Checkbox, TextInput } from "opus-react";3−import { Button, Card, TextInput } from "opus-react";4  5 type Customer = {6   id: string;7   name: string;8   company: string;9   status: "lead" | "active" | "paused";10 };11  12 type CustomerListProps = {13   customers: Customer[];14   onArchive: (customerId: string) => Promise<void>;15 };16  17 export function CustomerList({ customers, onArchive }: CustomerListProps) {18   const [query, setQuery] = useState("");19   const [selectedIds, setSelectedIds] = useState<string[]>([]);21+  const [isArchiving, setIsArchiving] = useState(false);22+  const [error, setError] = useState<string | null>(null);20  24+  const visibleCustomers = useMemo(() => {25+    const search = query.trim().toLowerCase();26+ 27+    if (!search) {28+      return customers;29+    }30+ 31+    return customers.filter((customer) =>32+      [customer.name, customer.company, customer.status]33+        .join(" ")34+        .toLowerCase()35+        .includes(search),36+    );37+  }, [customers, query]);21−  const visibleCustomers = customers.filter((customer) => {22−    return customer.name.toLowerCase().includes(query.toLowerCase());23−  });24  25   function toggleCustomer(customerId: string) {40+    setSelectedIds((current) =>41+      current.includes(customerId)42+        ? current.filter((id) => id !== customerId)43+        : [...current, customerId],44+    );26−    selectedIds.push(customerId);27−    setSelectedIds(selectedIds);28   }29  30   async function archiveSelected() {48+    setError(null);49+    setIsArchiving(true);31−    selectedIds.forEach(async (customerId) => {32−      await onArchive(customerId);33−    });34  51+    try {52+      await Promise.all(53+        selectedIds.map((customerId) => onArchive(customerId)),54+      );55+      setSelectedIds([]);56+    } catch {57+      setError("Some customers could not be archived. Please try again.");58+    } finally {59+      setIsArchiving(false);60+    }35−    setSelectedIds([]);36   }37  38   return (64+    <section aria-labelledby="customer-list-title">39−    <section>40       <header>66+        <h2 id="customer-list-title">Customers</h2>67+        <Button68+          disabled={selectedIds.length === 0 || isArchiving}69+          onClick={archiveSelected}70+        >71+          {isArchiving ? "Archiving…" : "Archive selected"}41−        <h2>Customers</h2>42−        <Button onclick={archiveSelected}>43−          Archive selected44         </Button>45       </header>46  47       <TextInput76+        id="customer-search"77+        label="Search customers"48         placeholder="Name, company or status"49         value={query}50         onChange={(event) => setQuery(event.target.value)}51       />52  83+      {error ? <p role="alert">{error}</p> : null}84+ 85+      <p role="status">86+        Showing {visibleCustomers.length} of {customers.length} customers87+      </p>88+ 89+      <div aria-busy={isArchiving}>53−      <div>54         {visibleCustomers.map((customer) => (91+          <Card key={customer.id}>92+            <Checkbox93+              aria-label={`Select ${customer.name}`}55−          <Card>56−            <input57−              type="checkbox"58               checked={selectedIds.includes(customer.id)}59               onChange={() => toggleCustomer(customer.id)}60             />61             <strong>{customer.name}</strong>62             <span>{customer.company}</span>63             <small>{customer.status}</small>64           </Card>65         ))}66       </div>67     </section>68   );69 }