Grid

7 blocks

Grid 01

Preview
npx nateui@latest add EmployeeDirectoryGrid
Dark
import { useMemo, useState } from "react";
import Avatar from "@/components/ui/Avatar";
import Button from "@/components/ui/Button";
import Card from "@/components/ui/Card";
import Dropdown from "@/components/ui/Dropdown";
import Input from "@/components/ui/Input";
import Pagination from "@/components/ui/Pagination";
import Select from "@/components/ui/Select";
import {
    PiArrowsDownUp,
    PiBriefcase,
    PiCaretDown,
    PiDotsThreeVerticalBold,
    PiEnvelopeSimple,
    PiMagnifyingGlass,
    PiPhone,
    PiSliders,
} from "react-icons/pi";

const assetBase = "https://statics.nateui.com/img";

type Employee = {
    id: string;
    name: string;
    role: string;
    email: string;
    phone: string;
    avatar: string;
};

const employees: Employee[] = [
    {
        id: "e1",
        name: "Priya Chandran",
        role: "Product Designer",
        email: "priya.chandran@corvexlabs.com",
        phone: "+1 (415) 555-0148",
        avatar: `${assetBase}/avatars/thumb-1.jpg`,
    },
    {
        id: "e2",
        name: "Oliver Bennett",
        role: "Software Engineer",
        email: "oliver.bennett@corvexlabs.com",
        phone: "+44 20 7946 0958",
        avatar: `${assetBase}/avatars/thumb-2.jpg`,
    },
    {
        id: "e3",
        name: "Naledi Khumalo",
        role: "HR Business Partner",
        email: "naledi.khumalo@corvexlabs.com",
        phone: "+27 11 234 5678",
        avatar: `${assetBase}/avatars/thumb-3.jpg`,
    },
    {
        id: "e4",
        name: "Diego Fuentes",
        role: "Marketing Manager",
        email: "diego.fuentes@corvexlabs.com",
        phone: "+1 (312) 555-0199",
        avatar: `${assetBase}/avatars/thumb-4.jpg`,
    },
    {
        id: "e5",
        name: "Hana Kobayashi",
        role: "Financial Analyst",
        email: "hana.kobayashi@corvexlabs.com",
        phone: "+81 3 4567 8901",
        avatar: `${assetBase}/avatars/thumb-5.jpg`,
    },
    {
        id: "e6",
        name: "Liam O'Connor",
        role: "Sales Representative",
        email: "liam.oconnor@corvexlabs.com",
        phone: "+1 (646) 555-0173",
        avatar: `${assetBase}/avatars/thumb-6.jpg`,
    },
    {
        id: "e7",
        name: "Fatima Al-Sayed",
        role: "Operations Lead",
        email: "fatima.alsayed@corvexlabs.com",
        phone: "+49 30 9876 5432",
        avatar: `${assetBase}/avatars/thumb-7.jpg`,
    },
    {
        id: "e8",
        name: "Marcus Delgado",
        role: "DevOps Engineer",
        email: "marcus.delgado@corvexlabs.com",
        phone: "+1 (503) 555-0166",
        avatar: `${assetBase}/avatars/thumb-8.jpg`,
    },
    {
        id: "e9",
        name: "Ingrid Sorensen",
        role: "Content Strategist",
        email: "ingrid.sorensen@corvexlabs.com",
        phone: "+46 8 123 456 78",
        avatar: `${assetBase}/avatars/thumb-9.jpg`,
    },
    {
        id: "e10",
        name: "Tariq Osei",
        role: "Account Executive",
        email: "tariq.osei@corvexlabs.com",
        phone: "+233 30 222 5566",
        avatar: `${assetBase}/avatars/thumb-10.jpg`,
    },
    {
        id: "e11",
        name: "Chloe Bergstrom",
        role: "Data Analyst",
        email: "chloe.bergstrom@corvexlabs.com",
        phone: "+1 (415) 555-0122",
        avatar: `${assetBase}/avatars/thumb-11.jpg`,
    },
    {
        id: "e12",
        name: "Rafael Nunes",
        role: "Support Specialist",
        email: "rafael.nunes@corvexlabs.com",
        phone: "+34 91 234 5678",
        avatar: `${assetBase}/avatars/thumb-12.jpg`,
    },
    {
        id: "e13",
        name: "Sienna Marlowe",
        role: "QA Engineer",
        email: "sienna.marlowe@corvexlabs.com",
        phone: "+1 (773) 555-0142",
        avatar: `${assetBase}/avatars/thumb-13.jpg`,
    },
    {
        id: "e14",
        name: "Kwame Asante",
        role: "Talent Acquisition Lead",
        email: "kwame.asante@corvexlabs.com",
        phone: "+971 4 123 4567",
        avatar: `${assetBase}/avatars/thumb-14.jpg`,
    },
    {
        id: "e15",
        name: "Noor Haddad",
        role: "Finance Manager",
        email: "noor.haddad@corvexlabs.com",
        phone: "+1 (212) 555-0187",
        avatar: `${assetBase}/avatars/thumb-15.jpg`,
    },
    {
        id: "e16",
        name: "Theo Whitfield",
        role: "Growth Marketer",
        email: "theo.whitfield@corvexlabs.com",
        phone: "+61 2 8123 4567",
        avatar: `${assetBase}/avatars/thumb-16.jpg`,
    },
];

const employmentTypeOptions = ["Full-time", "Part-time", "Contract", "Intern"];
const departmentOptions = [
    "Engineering",
    "Design",
    "People",
    "Marketing",
    "Finance",
    "Sales",
    "Operations",
];
const pageSizeOptions = [
    { value: 8, label: "8 / page" },
    { value: 16, label: "16 / page" },
];

export default function EmployeeDirectoryGrid() {
    const [search, setSearch] = useState("");
    const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
    const [pageSize, setPageSize] = useState(8);
    const [page, setPage] = useState(1);

    const filteredEmployees = useMemo(() => {
        const query = search.trim().toLowerCase();
        const matched = query
            ? employees.filter(
                  (employee) =>
                      employee.name.toLowerCase().includes(query) ||
                      employee.role.toLowerCase().includes(query) ||
                      employee.email.toLowerCase().includes(query),
              )
            : employees;

        return [...matched].sort((a, b) =>
            sortOrder === "asc"
                ? a.name.localeCompare(b.name)
                : b.name.localeCompare(a.name),
        );
    }, [search, sortOrder]);

    const pageCount = Math.max(
        1,
        Math.ceil(filteredEmployees.length / pageSize),
    );
    const currentPage = Math.min(page, pageCount);
    const pageItems = filteredEmployees.slice(
        (currentPage - 1) * pageSize,
        currentPage * pageSize,
    );
    const currentPageSizeOption =
        pageSizeOptions.find((option) => option.value === pageSize) ??
        pageSizeOptions[0];

    return (
        <section aria-label="Employee directory" className="w-full space-y-4">
            <div>
                <h4 className="text-xl font-semibold text-foreground">
                    Employees
                </h4>
            </div>

            <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
                <div className="flex flex-1 flex-wrap items-center gap-2">
                    <Input
                        prefix={<PiMagnifyingGlass />}
                        placeholder="Search employees..."
                        value={search}
                        onChange={(event) => {
                            setSearch(event.target.value);
                            setPage(1);
                        }}
                        className="w-full sm:w-64"
                    />

                    <Dropdown
                        placement="bottom-start"
                        renderTitle={
                            <Button variant="default" icon={<PiSliders />}>
                                Employment Type
                            </Button>
                        }
                    >
                        {employmentTypeOptions.map((option) => (
                            <Dropdown.Item key={option} eventKey={option}>
                                {option}
                            </Dropdown.Item>
                        ))}
                    </Dropdown>

                    <Dropdown
                        placement="bottom-start"
                        renderTitle={
                            <Button variant="default" icon={<PiSliders />}>
                                Department
                            </Button>
                        }
                    >
                        {departmentOptions.map((option) => (
                            <Dropdown.Item key={option} eventKey={option}>
                                {option}
                            </Dropdown.Item>
                        ))}
                    </Dropdown>
                </div>

                <div className="flex flex-wrap items-center gap-2">
                    <Dropdown
                        placement="bottom-end"
                        activeKey={sortOrder}
                        onSelect={(eventKey) => {
                            setSortOrder(eventKey as "asc" | "desc");
                            setPage(1);
                        }}
                        renderTitle={
                            <Button variant="default" icon={<PiArrowsDownUp />}>
                                Sort
                            </Button>
                        }
                    >
                        <Dropdown.Item eventKey="asc">Name (A–Z)</Dropdown.Item>
                        <Dropdown.Item eventKey="desc">
                            Name (Z–A)
                        </Dropdown.Item>
                    </Dropdown>

                    <Dropdown
                        placement="bottom-end"
                        renderTitle={
                            <Button
                                variant="solid"
                                icon={<PiCaretDown />}
                                iconAlignment="end"
                            >
                                Add Employee
                            </Button>
                        }
                    >
                        <Dropdown.Item eventKey="manual">
                            Add manually
                        </Dropdown.Item>
                        <Dropdown.Item eventKey="import">
                            Import from CSV
                        </Dropdown.Item>
                    </Dropdown>
                </div>
            </div>

            <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
                {pageItems.map((employee) => (
                    <Card key={employee.id} className="group relative">
                        <div className="flex items-center gap-3">
                            <Avatar
                                size="md"
                                src={employee.avatar}
                                alt={employee.name}
                            />
                            <span className="min-w-0 flex-1 truncate font-medium text-card-foreground">
                                {employee.name}
                            </span>
                            <Dropdown
                                placement="bottom-end"
                                renderTitle={
                                    <Button
                                        variant="ghost"
                                        size="sm"
                                        icon={<PiDotsThreeVerticalBold />}
                                        aria-label={`More actions for ${employee.name}`}
                                        className="opacity-0 transition-opacity focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100"
                                    />
                                }
                            >
                                <Dropdown.Item eventKey="view">
                                    View profile
                                </Dropdown.Item>
                                <Dropdown.Item eventKey="edit">
                                    Edit details
                                </Dropdown.Item>
                                <Dropdown.Item eventKey="remove">
                                    Remove
                                </Dropdown.Item>
                            </Dropdown>
                        </div>

                        <div className="mt-4 space-y-2">
                            <div className="flex items-center gap-2">
                                <PiBriefcase className="text-base" />
                                <span className="truncate">
                                    {employee.role}
                                </span>
                            </div>
                            <div className="flex items-center gap-2">
                                <PiEnvelopeSimple className="text-base" />
                                <span className="truncate">
                                    {employee.email}
                                </span>
                            </div>
                            <div className="flex items-center gap-2">
                                <PiPhone className="text-base" />
                                <span className="truncate">
                                    {employee.phone}
                                </span>
                            </div>
                        </div>
                    </Card>
                ))}
            </div>

            <div className="flex flex-wrap items-center justify-between gap-4">
                <Pagination
                    total={filteredEmployees.length}
                    pageSize={pageSize}
                    currentPage={currentPage}
                    onChange={(pageNumber) => setPage(pageNumber)}
                />

                <Select
                    className="w-32"
                    value={currentPageSizeOption}
                    options={pageSizeOptions}
                    onChange={(option) => {
                        setPageSize(option.value);
                        setPage(1);
                    }}
                />
            </div>
        </section>
    );
}

Grid 02

Preview
npx nateui@latest add IntegrationSettingsGrid
Dark
import { useMemo, useState } from 'react';
import Avatar from '@/components/ui/Avatar';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Switcher from '@/components/ui/Switcher';

const assetBase = 'https://statics.nateui.com/img';

type IntegrationType = 'automation' | 'storageCollaboration';

type Integration = {
  id: string;
  name: string;
  desc: string;
  logo: string;
  type: IntegrationType;
  active: boolean;
};

const initialIntegrations: Integration[] = [
  { id: 'google-drive', name: 'Google Drive', desc: 'Upload your files to Google Drive', logo: `${assetBase}/thumbs/brands/google-drive.png`, type: 'storageCollaboration', active: true },
  { id: 'github', name: 'Github', desc: 'Exchange files with a GitHub repository', logo: `${assetBase}/thumbs/brands/github.png`, type: 'storageCollaboration', active: true },
  { id: 'zapier', name: 'Zapier', desc: 'Integrate with hundreds of services.', logo: `${assetBase}/thumbs/brands/zapier.png`, type: 'automation', active: false },
  { id: 'make', name: 'Make (Integromat)', desc: 'Visually automate your workflows with Make', logo: `${assetBase}/thumbs/brands/make.png`, type: 'automation', active: false },
  { id: 'pabbly', name: 'Pabbly Connect', desc: 'Affordable automation for SaaS and CRM tools', logo: `${assetBase}/thumbs/brands/pabbly.png`, type: 'automation', active: false },
  { id: 'slack', name: 'Slack', desc: 'Post to a Slack channel', logo: `${assetBase}/thumbs/brands/slack.png`, type: 'storageCollaboration', active: false },
  { id: 'notion', name: 'Notion', desc: 'Retrieve notion note to your project', logo: `${assetBase}/thumbs/brands/notion.png`, type: 'storageCollaboration', active: false },
  { id: 'dropbox', name: 'Dropbox', desc: 'Exchange data with Dropbox', logo: `${assetBase}/thumbs/brands/dropbox.png`, type: 'storageCollaboration', active: false },
];

const sectionCopy: Record<IntegrationType, { title: string; desc: string }> = {
  automation: {
    title: 'Automation',
    desc: 'Automate tasks and connect multiple apps to streamline your workflow without manual effort.',
  },
  storageCollaboration: {
    title: 'Storage & Collaboration',
    desc: 'Tools that help you manage files, documents, and collaborate seamlessly across your team.',
  },
};

function IntegrationCard({ integration, onToggle }: { integration: Integration; onToggle: (id: string) => void }) {
  return (
    <Card
      bodyClass="flex flex-col gap-4 p-4"
      footer={{ className: 'flex items-center justify-between gap-2', content: (
        <>
          <Button>{integration.active ? 'Manage' : 'Learn More'}</Button>
          <Switcher checked={integration.active} onChange={() => onToggle(integration.id)} />
        </>
      ) }}
    >
      <Avatar shape="round" size={30} src={integration.logo} className="border-0 bg-transparent" />
      <div>
        <div className="font-semibold text-card-foreground">{integration.name}</div>
        <p className="text-muted-foreground">{integration.desc}</p>
      </div>
    </Card>
  );
}

export default function IntegrationSettingsGrid() {
  const [integrations, setIntegrations] = useState(initialIntegrations);

  const handleToggle = (id: string) => {
    setIntegrations((prev) => prev.map((item) => (item.id === id ? { ...item, active: !item.active } : item)));
  };

  const connected = useMemo(() => integrations.filter((item) => item.active), [integrations]);
  const grouped = useMemo(
    () => ({
      automation: integrations.filter((item) => !item.active && item.type === 'automation'),
      storageCollaboration: integrations.filter((item) => !item.active && item.type === 'storageCollaboration'),
    }),
    [integrations],
  );

  return (
    <section aria-label="Integrations" className="w-full space-y-8">
      <div className="border-b pb-4">
        <h4 className="text-xl font-semibold text-foreground">Integrations</h4>
      </div>

      {connected.length > 0 && (
        <div>
          <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
            {connected.map((integration) => (
              <IntegrationCard key={integration.id} integration={integration} onToggle={handleToggle} />
            ))}
          </div>
        </div>
      )}

      {(Object.keys(sectionCopy) as IntegrationType[]).map(
        (type) =>
          grouped[type].length > 0 && (
            <div key={type}>
              <div className="mb-4">
                <h5 className="text-lg font-semibold text-foreground">{sectionCopy[type].title}</h5>
                <p className="text-muted-foreground">{sectionCopy[type].desc}</p>
              </div>
              <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
                {grouped[type].map((integration) => (
                  <IntegrationCard key={integration.id} integration={integration} onToggle={handleToggle} />
                ))}
              </div>
            </div>
          ),
      )}
    </section>
  );
}

Grid 03

Preview
npx nateui@latest add FileManagerGrid
Dark
import { useState } from 'react';
import Avatar from '@/components/ui/Avatar';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Dropdown from '@/components/ui/Dropdown';
import Table from '@/components/ui/Table';
import FileIcon from '@/components/composites/FileIcon';
import { PiDotsThreeVerticalBold, PiPlusCircle } from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type Folder = {
  id: string;
  name: string;
  files: number;
  size: string;
};

const folders: Folder[] = [
  { id: 'health-report', name: 'Health Report', files: 80, size: '168 MB' },
  { id: 'medical-information', name: 'Medical Information', files: 8, size: '56 MB' },
  { id: 'prescriptions', name: 'Prescriptions', files: 20, size: '11 MB' },
  { id: 'archived', name: 'Archived', files: 99, size: '267 MB' },
];

type RecentFile = {
  id: string;
  name: string;
  date: string;
  size: string;
  fileType: string;
};

const recentFiles: RecentFile[] = [
  { id: 'recent-health-data', name: 'Health data', date: '14.02.2026', size: '56 MB', fileType: 'pdf' },
  { id: 'recent-medical-report', name: 'Medical report', date: '12.02.2026', size: '41 MB', fileType: 'docx' },
  { id: 'recent-prescriptions', name: 'Prescriptions', date: '09.02.2026', size: '18 MB', fileType: 'pdf' },
];

type FileRow = {
  id: string;
  name: string;
  date: string;
  fileType: string;
  uploaderName: string;
  uploaderAvatar: string;
};

const files: FileRow[] = [
  { id: 'file-1', name: 'Health Data', date: '14.02.2026', fileType: 'pdf', uploaderName: 'Amara Solis', uploaderAvatar: `${assetBase}/avatars/thumb-17.jpg` },
  { id: 'file-2', name: 'Medical Reports', date: '12.02.2026', fileType: 'docx', uploaderName: 'Dev Patel', uploaderAvatar: `${assetBase}/avatars/thumb-18.jpg` },
  { id: 'file-3', name: 'Prescription Log', date: '09.02.2026', fileType: 'pdf', uploaderName: 'Wren Castellano', uploaderAvatar: `${assetBase}/avatars/thumb-19.jpg` },
  { id: 'file-4', name: 'Lab Results', date: '05.02.2026', fileType: 'xlsx', uploaderName: 'Lucia Ferreira', uploaderAvatar: `${assetBase}/avatars/thumb-20.jpg` },
];

export default function FileManagerGrid() {
  const [selectedRecentFileId, setSelectedRecentFileId] = useState<string | null>(null);
  const [selectedFileId, setSelectedFileId] = useState<string | null>(null);

  return (
    <section aria-label="Documents" className="w-full space-y-8">
      <div>
        <div className="border-b pb-4">
          <h4 className="text-xl font-semibold text-foreground">Documents</h4>
        </div>
        <div className="mt-4">
          <Button variant="solid" icon={<PiPlusCircle />}>
            New file / Folder
          </Button>
        </div>
      </div>

      <div>
        <h5 className="mb-4 text-lg font-semibold text-foreground">Folders</h5>
        <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
          {folders.map((folder) => (
            <button
              key={folder.id}
              type="button"
              className="flex w-full flex-col gap-2 rounded-card border p-4 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
            >
              <FileIcon type="directory" size={40} />
              <div>
                <div className="font-semibold text-foreground">{folder.name}</div>
                <p className="text-muted-foreground">
                  {folder.files} Files • {folder.size}
                </p>
              </div>
            </button>
          ))}
        </div>
      </div>

      <div>
        <h5 className="mb-4 text-lg font-semibold text-foreground">Recent files</h5>
        <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
          {recentFiles.map((file) => {
            const isSelected = file.id === selectedRecentFileId;
            return (
              <Card
                key={file.id}
                clickable
                role="button"
                tabIndex={0}
                onClick={() => setSelectedRecentFileId(file.id)}
                onKeyDown={(event) => {
                  if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    setSelectedRecentFileId(file.id);
                  }
                }}
                aria-selected={isSelected}
                className={`transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${
                  isSelected ? 'bg-primary-soft' : 'hover:bg-accent'
                }`}
                bodyClass="flex items-center gap-2 p-4"
              >
                <FileIcon type={file.fileType} size={40} />
                <div className="min-w-0">
                  <div className="truncate font-medium text-card-foreground">{file.name}</div>
                  <p className="truncate text-muted-foreground">
                    {file.date} • {file.size}
                  </p>
                </div>
              </Card>
            );
          })}
        </div>
      </div>

      <div>
        <h5 className="mb-4 text-lg font-semibold text-foreground">All Files</h5>
        <div className="overflow-hidden rounded-card border bg-card">
        <Table>
          <Table.THead>
            <Table.Tr>
              <Table.Th>Name</Table.Th>
              <Table.Th>Uploaded Date</Table.Th>
              <Table.Th>Uploaded By</Table.Th>
              <Table.Th>More Actions</Table.Th>
            </Table.Tr>
          </Table.THead>
          <Table.TBody>
            {files.map((file) => {
              const isSelected = file.id === selectedFileId;
              return (
                <Table.Tr
                  key={file.id}
                  tabIndex={0}
                  onClick={() => setSelectedFileId(file.id)}
                  onKeyDown={(event) => {
                    if (event.key === 'Enter' || event.key === ' ') {
                      event.preventDefault();
                      setSelectedFileId(file.id);
                    }
                  }}
                  aria-selected={isSelected}
                  className={`cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
                    isSelected ? 'bg-primary-soft hover:bg-primary-soft' : ''
                  }`}
                >
                  <Table.Td>
                    <div className="flex items-center gap-2">
                      <FileIcon type={file.fileType} size={28} />
                      <span className="font-medium text-foreground">{file.name}</span>
                    </div>
                  </Table.Td>
                  <Table.Td className="text-muted-foreground">{file.date}</Table.Td>
                  <Table.Td>
                    <div className="flex items-center gap-2">
                      <Avatar size={24} src={file.uploaderAvatar} alt={file.uploaderName} />
                      <span className="text-foreground">{file.uploaderName}</span>
                    </div>
                  </Table.Td>
                  <Table.Td>
                    <div className="flex justify-end" onClick={(event) => event.stopPropagation()}>
                      <Dropdown
                        placement="bottom-end"
                        renderTitle={
                          <Button
                            variant="ghost"
                            shape="circle"
                            size="sm"
                            icon={<PiDotsThreeVerticalBold />}
                            aria-label={`More actions for ${file.name}`}
                          />
                        }
                      >
                        <Dropdown.Item eventKey="open">Open</Dropdown.Item>
                        <Dropdown.Item eventKey="rename">Rename</Dropdown.Item>
                        <Dropdown.Item eventKey="delete">Delete</Dropdown.Item>
                      </Dropdown>
                    </div>
                  </Table.Td>
                </Table.Tr>
              );
            })}
          </Table.TBody>
        </Table>
        </div>
      </div>
    </section>
  );
}

Grid 04

Preview
npx nateui@latest add ProductGrid
Dark
import { useMemo, useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Dropdown from '@/components/ui/Dropdown';
import Input from '@/components/ui/Input';
import Pagination from '@/components/ui/Pagination';
import Segment from '@/components/ui/Segment';
import Select from '@/components/ui/Select';
import {
  PiDotsThreeVerticalBold,
  PiFunnel,
  PiMagnifyingGlass,
  PiPlus,
  PiStarFill,
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type Product = {
  id: string;
  name: string;
  price: number;
  rating: number;
  status: 'active' | 'inactive';
  image: string;
};

const products: Product[] = [
  { id: 'p1', name: '16-inch Pro Laptop', price: 1999, rating: 4.8, status: 'active', image: `${assetBase}/thumbs/products/product-1.jpg` },
  { id: 'p2', name: 'True Wireless Earbuds', price: 129, rating: 4.5, status: 'active', image: `${assetBase}/thumbs/products/product-9.jpg` },
  { id: 'p3', name: 'Sport Mini Backpack', price: 68, rating: 4.3, status: 'active', image: `${assetBase}/thumbs/products/product-3.jpg` },
  { id: 'p4', name: 'Court Classic Sneaker', price: 95, rating: 4.6, status: 'active', image: `${assetBase}/thumbs/products/product-6.jpg` },
  { id: 'p5', name: 'Dive Chronograph Watch', price: 310, rating: 4.9, status: 'inactive', image: `${assetBase}/thumbs/products/product-10.jpg` },
  { id: 'p6', name: 'Crossbody Camera Bag', price: 145, rating: 4.4, status: 'active', image: `${assetBase}/thumbs/products/product-14.jpg` },
  { id: 'p7', name: 'Hydrating Glow Serum', price: 42, rating: 4.2, status: 'inactive', image: `${assetBase}/thumbs/products/product-15.jpg` },
  { id: 'p8', name: 'Over-Ear Studio Headphones', price: 89, rating: 4.5, status: 'active', image: `${assetBase}/thumbs/products/product-18.jpg` },
  { id: 'p9', name: 'Leather Wallet & Keyfob Set', price: 76, rating: 4.7, status: 'active', image: `${assetBase}/thumbs/products/product-19.jpg` },
  { id: 'p10', name: 'Fleece Pullover Hoodie', price: 58, rating: 4.3, status: 'inactive', image: `${assetBase}/thumbs/products/product-20.jpg` },
  { id: 'p11', name: 'Aluminum Smartwatch', price: 399, rating: 4.6, status: 'active', image: `${assetBase}/thumbs/products/product-2.jpg` },
  { id: 'p12', name: 'Cotton Overshirt Jacket', price: 84, rating: 4.4, status: 'active', image: `${assetBase}/thumbs/products/product-11.jpg` },
];

const filterOptions = ['Category', 'Price', 'Rating', 'Stock'];
const pageSizeOptions = [
  { value: 8, label: '8 / page' },
  { value: 12, label: '12 / page' },
];

export default function ProductGrid() {
  const [statusFilter, setStatusFilter] = useState('all');
  const [search, setSearch] = useState('');
  const [pageSize, setPageSize] = useState(8);
  const [page, setPage] = useState(1);

  const filteredProducts = useMemo(() => {
    const query = search.trim().toLowerCase();
    return products.filter((product) => {
      const matchesStatus = statusFilter === 'all' || product.status === statusFilter;
      const matchesQuery = !query || product.name.toLowerCase().includes(query);
      return matchesStatus && matchesQuery;
    });
  }, [search, statusFilter]);

  const pageCount = Math.max(1, Math.ceil(filteredProducts.length / pageSize));
  const currentPage = Math.min(page, pageCount);
  const pageItems = filteredProducts.slice((currentPage - 1) * pageSize, currentPage * pageSize);
  const currentPageSizeOption = pageSizeOptions.find((option) => option.value === pageSize) ?? pageSizeOptions[0];

  return (
    <section aria-label="Products" className="w-full space-y-4">
      <div className="border-b pb-4">
        <h4 className="text-xl font-semibold text-foreground">Products</h4>
      </div>

      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <Segment
          value={statusFilter}
          onChange={(value) => {
            setStatusFilter(value as string);
            setPage(1);
          }}
        >
          <Segment.Item value="all">All</Segment.Item>
          <Segment.Item value="active">Active</Segment.Item>
          <Segment.Item value="inactive">Non Active</Segment.Item>
        </Segment>

        <div className="flex flex-wrap items-center gap-2">
          <Input
            prefix={<PiMagnifyingGlass />}
            placeholder="Search product"
            value={search}
            onChange={(event) => {
              setSearch(event.target.value);
              setPage(1);
            }}
            className="w-full sm:w-56"
          />

          <Dropdown
            placement="bottom-end"
            renderTitle={
              <Button variant="default" icon={<PiFunnel />}>
                Filter
              </Button>
            }
          >
            {filterOptions.map((option) => (
              <Dropdown.Item key={option} eventKey={option}>
                {option}
              </Dropdown.Item>
            ))}
          </Dropdown>

          <Button variant="solid" icon={<PiPlus />}>
            New Product
          </Button>
        </div>
      </div>

      <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
        {pageItems.map((product) => (
          <Card key={product.id}>
            <div className="relative aspect-square overflow-hidden rounded-card">
              <div className="flex h-full items-center justify-center">
                <img src={product.image} alt={product.name} className="max-h-full max-w-full object-contain" />
              </div>
              <Dropdown
                placement="bottom-end"
                toggleClassName="absolute right-2 top-2"
                renderTitle={
                  <Button
                    shape="circle"
                    size="sm"
                    icon={<PiDotsThreeVerticalBold />}
                    aria-label={`More actions for ${product.name}`}
                  />
                }
              >
                <Dropdown.Item eventKey="edit">Edit</Dropdown.Item>
                <Dropdown.Item eventKey="duplicate">Duplicate</Dropdown.Item>
                <Dropdown.Item eventKey="delete">Delete</Dropdown.Item>
              </Dropdown>
            </div>

            <div className="mt-2">
              <div className="truncate font-medium text-card-foreground">{product.name}</div>
              <div className="mt-1 flex items-center justify-between">
                <span className="text-foreground">${product.price.toFixed(2)}</span>
                <span className="flex items-center gap-1 text-muted-foreground">
                  <PiStarFill className="text-palette-yellow" />
                  {product.rating}
                </span>
              </div>
            </div>
          </Card>
        ))}
      </div>

      <div className="flex flex-wrap items-center justify-between gap-4">
        <Pagination
          total={filteredProducts.length}
          pageSize={pageSize}
          currentPage={currentPage}
          onChange={(pageNumber) => setPage(pageNumber)}
        />

        <Select
          className="w-32"
          value={currentPageSizeOption}
          options={pageSizeOptions}
          onChange={(option) => {
            setPageSize(option.value);
            setPage(1);
          }}
        />
      </div>
    </section>
  );
}

Grid 05

Preview
npx nateui@latest add MediaLibraryGrid
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Menu from '@/components/ui/Menu';
import FileIcon from '@/components/composites/FileIcon';
import {
  PiCheckBold,
  PiFolder,
  PiFolderFill,
  PiMagnifyingGlass,
  PiPlus,
  PiUploadSimple,
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type Folder = {
  id: string;
  name: string;
};

const folders: Folder[] = [
  { id: 'recent', name: 'Recent' },
  { id: 'product-shots', name: 'Product Shots' },
  { id: 'illustrations', name: 'Illustrations' },
  { id: 'archive', name: 'Archive' },
];

type MediaItem =
  | { id: string; kind: 'image'; src: string }
  | { id: string; kind: 'file'; name: string; fileType: string };

const mediaItems: MediaItem[] = [
  { id: 'm1', kind: 'image', src: `${assetBase}/thumbs/misc/img-1.png` },
  { id: 'm2', kind: 'image', src: `${assetBase}/thumbs/misc/img-2.png` },
  { id: 'm3', kind: 'image', src: `${assetBase}/thumbs/misc/img-3.png` },
  { id: 'm4', kind: 'image', src: `${assetBase}/thumbs/misc/img-4.png` },
  { id: 'm5', kind: 'file', name: 'Report.doc', fileType: 'doc' },
  { id: 'm6', kind: 'image', src: `${assetBase}/thumbs/misc/img-5.png` },
  { id: 'm7', kind: 'image', src: `${assetBase}/thumbs/misc/img-6.png` },
  { id: 'm8', kind: 'file', name: 'Icons.zip', fileType: 'zip' },
  { id: 'm9', kind: 'file', name: 'Poster Layout.ai', fileType: 'ai' },
  { id: 'm10', kind: 'file', name: 'Logo Concepts.ai', fileType: 'ai' },
  { id: 'm11', kind: 'image', src: `${assetBase}/thumbs/misc/img-12.png` },
  { id: 'm12', kind: 'image', src: `${assetBase}/thumbs/misc/img-13.png` },
];

export default function MediaLibraryGrid() {
  const [activeFolder, setActiveFolder] = useState('illustrations');
  const [search, setSearch] = useState('');
  const [selectedIds, setSelectedIds] = useState<string[]>([]);

  function toggleSelected(id: string) {
    setSelectedIds((previous) =>
      previous.includes(id) ? previous.filter((existing) => existing !== id) : [...previous, id],
    );
  }

  return (
    <section aria-label="Media library" className="w-full space-y-4 px-2">
      <div className="border-b pb-4">
        <h4 className="text-xl font-semibold text-foreground">Media Library</h4>
      </div>

      <div className="flex flex-col gap-8 lg:flex-row lg:items-stretch">
        <div className="flex w-full flex-col lg:w-[220px] lg:flex-shrink-0">
          <div>
            <h6 className="mb-2 px-2 text-base font-medium text-foreground">Folders</h6>
            <nav className="pb-2 border-b mb-2">
              <Menu variant="subtle">
                {folders.map((folder) => {
                  const isActive = activeFolder === folder.id;
                  return (
                    <Menu.MenuItem
                      key={folder.id}
                      eventKey={folder.id}
                      isActive={isActive}
                      role="button"
                      tabIndex={0}
                      aria-current={isActive ? 'true' : undefined}
                      onClick={() => setActiveFolder(folder.id)}
                      onKeyDown={(event: React.KeyboardEvent) => {
                        if (event.key === 'Enter' || event.key === ' ') {
                          event.preventDefault();
                          setActiveFolder(folder.id);
                        }
                      }}
                      className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${
                        isActive ? 'font-medium' : ''
                      }`}
                    >
                      {isActive ? <PiFolderFill className="text-lg" /> : <PiFolder className="text-lg" />}
                      <span className="min-w-0 truncate">{folder.name}</span>
                    </Menu.MenuItem>
                  );
                })}
              </Menu>
            </nav>
            <Button variant="ghost" className="text-start w-full justify-start px-2">
              <span className="flex items-center gap-1">
                <PiPlus />
                New Folder
              </span>
            </Button>
          </div>

          <div className="mt-auto pt-8">
            <Button icon={<PiUploadSimple />} className="w-full">
              Upload File
            </Button>
          </div>
        </div>

        <div className="min-w-0 flex-1 space-y-4">
          <div className="flex justify-end">
            <Input
              prefix={<PiMagnifyingGlass />}
              placeholder="Search media"
              value={search}
              onChange={(event) => setSearch(event.target.value)}
              className="w-full sm:w-64"
            />
          </div>

          <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
            {mediaItems.map((item) => {
              const isSelected = selectedIds.includes(item.id);
              const label = item.kind === 'image' ? 'image' : item.name;

              return (
                <button
                  key={item.id}
                  type="button"
                  onClick={() => toggleSelected(item.id)}
                  aria-pressed={isSelected}
                  aria-label={isSelected ? `Deselect ${label}` : `Select ${label}`}
                  className={`group relative aspect-square overflow-hidden rounded-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${
                    isSelected ? 'ring-2 ring-primary' : ''
                  }`}
                >
                  {item.kind === 'image' ? (
                    <>
                      <img src={item.src} alt="" className="h-full w-full object-cover" />
                      <span className="absolute inset-0 bg-foreground/0 transition-colors group-hover:bg-foreground/5" />
                    </>
                  ) : (
                    <div className="flex h-full flex-col items-center justify-center gap-2 border bg-card p-4 text-center transition-colors group-hover:bg-accent">
                      <FileIcon type={item.fileType} size={40} />
                      <p className="w-full truncate text-sm font-medium text-card-foreground">{item.name}</p>
                    </div>
                  )}
                  {isSelected ? (
                    <span className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background">
                      <PiCheckBold className="text-sm" />
                    </span>
                  ) : null}
                </button>
              );
            })}
          </div>

          <div className="flex justify-end border-t pt-4">
            <Button variant="solid" disabled={selectedIds.length === 0}>
              Import Selected
            </Button>
          </div>
        </div>
      </div>
    </section>
  );
}

Grid 06

Preview
npx nateui@latest add ProjectPortfolioGrid
Dark
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Dropdown from '@/components/ui/Dropdown';
import Tag from '@/components/ui/Tag';
import UsersAvatarGroup from '@/components/composites/UsersAvatarGroup';
import {
  PiCheckCircle,
  PiDotsThreeVerticalBold,
  PiListChecks,
  PiPackage,
  PiPlus,
  PiProhibit,
  PiSpinner,
  PiStar,
  PiStarFill,
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type Priority = 'Low' | 'Medium' | 'High';
type Status = 'progress' | 'blocked' | 'done' | 'cancelled';

type Project = {
  id: string;
  name: string;
  description: string;
  image: string;
  status: Status;
  tasksDone: number;
  tasksTotal: number;
  priority: Priority;
  starred: boolean;
  members: { name: string; img: string }[];
};

const statusMap = {
  progress: { className: 'text-info', icon: <PiSpinner aria-hidden="true" /> },
  blocked: { className: 'text-destructive', icon: <PiProhibit aria-hidden="true" /> },
  done: { className: 'text-success', icon: <PiCheckCircle aria-hidden="true" /> },
  cancelled: { className: 'text-warning', icon: <PiPackage aria-hidden="true" /> },
};

const priorityIntentMap: Record<Priority, string> = {
  Low: 'bg-success',
  Medium: 'bg-warning',
  High: 'bg-destructive',
};

const projects: Project[] = [
  {
    id: 'p1',
    name: 'Onboarding Flow',
    description: 'Streamlining the new-hire signup flow to cut drop-off in the first session.',
    image: `${assetBase}/thumbs/projects/img-1.jpg`,
    status: 'progress',
    tasksDone: 9,
    tasksTotal: 14,
    priority: 'High',
    starred: true,
    members: [
      { name: 'Ava Brennan', img: `${assetBase}/avatars/thumb-1.jpg` },
      { name: 'Leo Marchetti', img: `${assetBase}/avatars/thumb-4.jpg` },
    ],
  },
  {
    id: 'p2',
    name: 'Contract Review',
    description: 'Paused while legal finalizes updated procurement terms.',
    image: `${assetBase}/thumbs/projects/img-2.jpg`,
    status: 'blocked',
    tasksDone: 2,
    tasksTotal: 8,
    priority: 'Medium',
    starred: false,
    members: [{ name: 'Priya Shah', img: `${assetBase}/avatars/thumb-3.jpg` }],
  },
  {
    id: 'p3',
    name: 'iOS Companion',
    description: 'Shipped the first release with push notifications and offline sync.',
    image: `${assetBase}/thumbs/projects/img-3.jpg`,
    status: 'done',
    tasksDone: 18,
    tasksTotal: 18,
    priority: 'High',
    starred: true,
    members: [
      { name: 'Naomi Osei', img: `${assetBase}/avatars/thumb-5.jpg` },
      { name: 'Theo Lindqvist', img: `${assetBase}/avatars/thumb-6.jpg` },
    ],
  },
  {
    id: 'p4',
    name: 'Data Migration',
    description: 'Shelved after the team standardized on a different storage provider.',
    image: `${assetBase}/thumbs/projects/img-4.jpg`,
    status: 'cancelled',
    tasksDone: 3,
    tasksTotal: 12,
    priority: 'Low',
    starred: false,
    members: [{ name: 'Owen Bright', img: `${assetBase}/avatars/thumb-2.jpg` }],
  },
  {
    id: 'p5',
    name: 'Billing Sync',
    description: 'Automating invoice matching against payment processor records.',
    image: `${assetBase}/thumbs/projects/img-5.jpg`,
    status: 'progress',
    tasksDone: 7,
    tasksTotal: 15,
    priority: 'Medium',
    starred: true,
    members: [
      { name: 'Maya Chen', img: `${assetBase}/avatars/thumb-7.jpg` },
      { name: 'Marcus Webb', img: `${assetBase}/avatars/thumb-8.jpg` },
    ],
  },
  {
    id: 'p6',
    name: 'Portal Refresh',
    description: 'Rebuilding the self-serve account pages for faster load times.',
    image: `${assetBase}/thumbs/projects/img-6.jpg`,
    status: 'progress',
    tasksDone: 13,
    tasksTotal: 19,
    priority: 'High',
    starred: true,
    members: [{ name: 'Ivy Chen', img: `${assetBase}/avatars/thumb-9.jpg` }],
  },
  {
    id: 'p7',
    name: 'Inventory Sync',
    description: 'Connecting store locations to a single real-time stock feed.',
    image: `${assetBase}/thumbs/projects/img-7.jpg`,
    status: 'progress',
    tasksDone: 6,
    tasksTotal: 9,
    priority: 'Medium',
    starred: false,
    members: [
      { name: 'Dana Ruiz', img: `${assetBase}/avatars/thumb-10.jpg` },
      { name: 'Tomas Reyes', img: `${assetBase}/avatars/thumb-11.jpg` },
    ],
  },
  {
    id: 'p8',
    name: 'Design Tokens',
    description: 'Finalized the shared color, spacing, and type scale for every product surface.',
    image: `${assetBase}/thumbs/projects/img-8.jpg`,
    status: 'done',
    tasksDone: 15,
    tasksTotal: 15,
    priority: 'Low',
    starred: true,
    members: [{ name: 'Grace Liu', img: `${assetBase}/avatars/thumb-12.jpg` }],
  },
  {
    id: 'p9',
    name: 'Voice Assistant',
    description: 'On hold while the team gathers more call-transcript training data.',
    image: `${assetBase}/thumbs/projects/img-9.jpg`,
    status: 'blocked',
    tasksDone: 5,
    tasksTotal: 11,
    priority: 'High',
    starred: true,
    members: [
      { name: 'Felix Bauer', img: `${assetBase}/avatars/thumb-13.jpg` },
      { name: 'Priya Nair', img: `${assetBase}/avatars/thumb-14.jpg` },
    ],
  },
  {
    id: 'p10',
    name: 'Auth Retirement',
    description: 'Deprioritized after the identity provider migration covered the same need.',
    image: `${assetBase}/thumbs/projects/img-10.jpg`,
    status: 'cancelled',
    tasksDone: 4,
    tasksTotal: 10,
    priority: 'Medium',
    starred: false,
    members: [{ name: 'Owen Blake', img: `${assetBase}/avatars/thumb-15.jpg` }],
  },
];

export default function ProjectPortfolioGrid() {
  return (
    <section aria-label="Projects" className="w-full space-y-4">
      <div className="flex flex-col gap-4 pb-4 sm:flex-row sm:items-center sm:justify-between">
        <div>
          <h4 className="text-xl font-semibold text-foreground">Projects</h4>
          <p className="mt-1 max-w-xl text-muted-foreground">
            Track every active initiative, review priority and progress, and jump into the ones that need
            attention.
          </p>
        </div>
        <Button variant="solid" icon={<PiPlus />} className="shrink-0">
          New Project
        </Button>
      </div>

      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {projects.map((project) => {
          const status = statusMap[project.status];

          return (
            <Card key={project.id}
              footer={{ className: 'flex items-center justify-between gap-2', content: (
                <>
                  <div className="flex flex-wrap items-center gap-2">
                    <span className={`text-base ${status.className}`} aria-label={project.status}>
                      {status.icon}
                    </span>
                    <span className="flex items-center gap-1 text-muted-foreground">
                      <PiListChecks className="text-base" />
                      {project.tasksDone}/{project.tasksTotal}
                    </span>
                    <Tag
                      className="gap-1 bg-transparent"
                      prefix={<span className={`h-2.5 w-2.5 rounded-xs ${priorityIntentMap[project.priority]}`} />}
                    >
                      {project.priority}
                    </Tag>
                  </div>
                  <UsersAvatarGroup
                    users={project.members}
                    avatarProps={{ size: 24 }}
                  />
                </>
              ) }}
            >
              <div className="flex justify-between">
                <div className="flex items-center justify-between gap-2">
                  <img src={project.image} alt="" className="h-6 w-6 shrink-0 rounded-lg object-cover" />
                    <div className="min-w-0 flex-1">
                      <h6 className="truncate font-semibold text-card-foreground">{project.name}</h6>
                    </div>
                </div>
                <div className="flex items-center">
                  <button
                    className="p-2 text-base"
                    aria-label={project.starred ? `Unstar ${project.name}` : `Star ${project.name}`}
                  >
                    {project.starred ? <PiStarFill className="text-palette-yellow" /> : <PiStar />}
                  </button>
                  <Dropdown
                    placement="bottom-end"
                    renderTitle={
                      <Button
                        size="sm"
                        variant="ghost"
                        icon={<PiDotsThreeVerticalBold />}
                        aria-label={`More actions for ${project.name}`}
                      />
                    }
                  >
                    <Dropdown.Item eventKey="view">View Details</Dropdown.Item>
                    <Dropdown.Item eventKey="duplicate">Duplicate</Dropdown.Item>
                    <Dropdown.Item eventKey="archive">Archive</Dropdown.Item>
                    <Dropdown.Item variant="divider" />
                    <Dropdown.Item eventKey="delete" className="text-destructive">
                      Delete
                    </Dropdown.Item>
                  </Dropdown>
                </div>
              </div>
              <p className="mt-4 line-clamp-2 text-muted-foreground">{project.description}</p>
            </Card>
          );
        })}
      </div>
    </section>
  );
}

Grid 07

Preview
npx nateui@latest add ServerInstanceGrid
Dark
import { useState } from 'react';
import Avatar from '@/components/ui/Avatar';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Dropdown from '@/components/ui/Dropdown';
import Input from '@/components/ui/Input';
import Tag from '@/components/ui/Tag';
import IconFrame from '@/components/composites/IconFrame';
import {
  PiCheck,
  PiCopySimple,
  PiDotsThreeVerticalBold,
  PiGlobe,
  PiMagnifyingGlass,
  PiMapPin,
  PiPauseFill,
  PiPlayFill,
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type ServerStatus = 'running' | 'stopped';

type ServerInstance = {
  id: string;
  name: string;
  specs: string;
  location: string;
  ip: string;
  status: ServerStatus;
  tags: string[];
  createdDaysAgo: number;
  logo: string;
};

const initialServers: ServerInstance[] = [
  {
    id: 'srv-1',
    name: 'Fedora-40',
    specs: '2GB DDR4 / 64GB SSD',
    location: 'ap-southeast-1',
    ip: '192.0.2.11',
    status: 'stopped',
    tags: [],
    createdDaysAgo: 5,
    logo: `${assetBase}/thumbs/brands/fedora.png`,
  },
  {
    id: 'srv-2',
    name: 'Ubuntu-22.04',
    specs: '4GB DDR4 / 80GB SSD',
    location: 'eu-west-1',
    ip: '192.0.2.24',
    status: 'running',
    tags: ['staging'],
    createdDaysAgo: 12,
    logo: `${assetBase}/thumbs/brands/ubuntu.png`,
  },
  {
    id: 'srv-3',
    name: 'RHEL-9',
    specs: '2GB DDR4 / 48GB SSD',
    location: 'us-east-1',
    ip: '192.0.2.37',
    status: 'running',
    tags: [],
    createdDaysAgo: 30,
    logo: `${assetBase}/thumbs/brands/red-hat.png`,
  },
  {
    id: 'srv-4',
    name: 'Fedora-38',
    specs: '8GB DDR4 / 160GB SSD',
    location: 'ap-southeast-2',
    ip: '192.0.2.48',
    status: 'stopped',
    tags: ['production'],
    createdDaysAgo: 2,
    logo: `${assetBase}/thumbs/brands/fedora.png`,
  },
  {
    id: 'srv-5',
    name: 'Ubuntu-20.04',
    specs: '2GB DDR4 / 64GB SSD',
    location: 'eu-north-1',
    ip: '192.0.2.53',
    status: 'running',
    tags: [],
    createdDaysAgo: 45,
    logo: `${assetBase}/thumbs/brands/ubuntu.png`,
  },
];

export default function ServerInstanceGrid() {
  const [servers, setServers] = useState(initialServers);
  const [search, setSearch] = useState('');
  const [copiedId, setCopiedId] = useState<string | null>(null);

  function toggleStatus(id: string) {
    setServers((previous) =>
      previous.map((server) =>
        server.id === id
          ? { ...server, status: server.status === 'running' ? 'stopped' : 'running' }
          : server,
      ),
    );
  }

  function copyIp(id: string, ip: string) {
    navigator.clipboard?.writeText(ip);
    setCopiedId(id);
    setTimeout(() => setCopiedId((current) => (current === id ? null : current)), 1500);
  }

  const query = search.trim().toLowerCase();
  const visibleServers = query
    ? servers.filter((server) => server.name.toLowerCase().includes(query))
    : servers;

  return (
    <section aria-label="Servers" className="w-full space-y-4">
      <div className="flex flex-col gap-4 border-b pb-4 sm:flex-row sm:items-center sm:justify-between">
        <h4 className="text-xl font-semibold text-foreground">Servers</h4>
        <Input
          prefix={<PiMagnifyingGlass />}
          placeholder="Search..."
          value={search}
          onChange={(event) => setSearch(event.target.value)}
          className="w-full sm:w-64"
        />
      </div>

      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {visibleServers.map((server) => {
          const isRunning = server.status === 'running';

          return (
            <Card
              key={server.id}
              footer={{
                className: 'flex items-center justify-between gap-2',
                content: (
                  <>
                    <div className="flex flex-wrap items-center gap-1.5">
                      {server.tags.length === 0 ? (
                        <Tag className="bg-muted text-muted-foreground">Untagged</Tag>
                      ) : (
                        server.tags.map((tag) => (
                          <Tag key={tag}>
                            {tag}
                          </Tag>
                        ))
                      )}
                    </div>
                    <span className="shrink-0 text-muted-foreground">Created {server.createdDaysAgo} days ago</span>
                  </>
                ),
              }}
            >
              <div className="flex items-start justify-between gap-2">
                <div className="flex min-w-0 items-center gap-3">
                  <IconFrame size={40} className="shrink-0">
                    <Avatar src={server.logo} alt={server.name} size={24} shape="square" className="border-0 bg-transparent" />
                  </IconFrame>
                  <div className="min-w-0">
                    <div className="truncate font-semibold text-card-foreground">{server.name}</div>
                    <p className="truncate text-xs text-muted-foreground">{server.specs}</p>
                  </div>
                </div>
                <div className="flex shrink-0 items-center gap-1">
                  <Button
                    size="sm"
                    variant="subtle"
                    className={
                      isRunning
                        ? 'text-destructive hover:text-destructive focus-visible:ring-destructive'
                        : 'text-success hover:text-success focus-visible:ring-success'
                    }
                    icon={isRunning ? <PiPauseFill /> : <PiPlayFill />}
                    onClick={() => toggleStatus(server.id)}
                    aria-label={isRunning ? `Stop ${server.name}` : `Start ${server.name}`}
                  />
                  <Dropdown
                    placement="bottom-end"
                    renderTitle={
                      <Button
                        size="sm"
                        variant="ghost"
                        icon={<PiDotsThreeVerticalBold />}
                        aria-label={`More actions for ${server.name}`}
                      />
                    }
                  >
                    <Dropdown.Item eventKey="restart">Restart</Dropdown.Item>
                    <Dropdown.Item eventKey="reinstall">Reinstall</Dropdown.Item>
                    <Dropdown.Item eventKey="details">View Details</Dropdown.Item>
                    <Dropdown.Item variant="divider" />
                    <Dropdown.Item eventKey="delete" className="text-destructive">
                      Delete
                    </Dropdown.Item>
                  </Dropdown>
                </div>
              </div>

              <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1">
                <span className="flex items-center gap-1">
                  <PiMapPin className="text-base" />
                  {server.location}
                </span>
                <span className="flex items-center gap-1">
                  <PiGlobe className="text-base" />
                  {server.ip}
                  <button
                    type="button"
                    onClick={() => copyIp(server.id, server.ip)}
                    aria-label={`Copy IP address for ${server.name}`}
                    className="text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                  >
                    {copiedId === server.id ? <PiCheck className="text-base" /> : <PiCopySimple className="text-base" />}
                  </button>
                </span>
              </div>
            </Card>
          );
        })}
      </div>
    </section>
  );
}