Filterable CRUD Table
A plant tracking app with CRUD operations, search, filters, table column resizing, form validation, and more.
 AgapanthusAgapanthus praecox
AgapanthusAgapanthus praecox AloeAloe vera
AloeAloe vera Blue JacarandaJacaranda mimosifolia
Blue JacarandaJacaranda mimosifolia Chinese Money PlantPilea peperomioides
Chinese Money PlantPilea peperomioides Christmas BushCeratopetalum gummiferum
Christmas BushCeratopetalum gummiferum Delta Maidenhair FernAdiantum raddianum
Delta Maidenhair FernAdiantum raddianum Fiddle Leaf FigFicus lyrata
Fiddle Leaf FigFicus lyrata GardeniaGardenia jasminoides
GardeniaGardenia jasminoides IvyHedera
IvyHedera Morning GloryIpomoea
Morning GloryIpomoea NasturtiumTropaeolum
NasturtiumTropaeolum OleanderNerium oleander
OleanderNerium oleander PoplarPopulus
PoplarPopulus Spider PlantChlorophytum comosum
Spider PlantChlorophytum comosum Split-leaf PhilodendronMonstera deliciosa
Split-leaf PhilodendronMonstera deliciosa Star JasmineTrachelospermum jasminoides
Star JasmineTrachelospermum jasminoides Striped DracaenaAsparagaceae
Striped DracaenaAsparagaceae Tree FernCyatheaceae
Tree FernCyatheaceae Tuberous Sword FernNephrolepis cordifolia
Tuberous Sword FernNephrolepis cordifolia Zanzibar GemZamioculcas
Zanzibar GemZamioculcasApp.tsx
Labels.tsx
PlantActionMenu.tsx
PlantDialog.tsx
PlantList.tsx
PlantTable.tsx
App.tsx
Labels.tsx
PlantActionMenu.tsx
PlantDialog.tsx
PlantList.tsx
PlantTable.tsx
App.tsx
Labels.tsx
PlantActionMenu.tsx
PlantDialog.tsx
PlantList.tsx
PlantTable.tsx
import {AlertDialog} from './AlertDialog';
import {Button} from './Button';
import {Checkbox} from './Checkbox';
import {FilterIcon, PlusIcon, RefreshCw, SlidersIcon} from 'lucide-react';
import {DialogTrigger, Heading, Key, Selection, SortDescriptor, TooltipTrigger} from 'react-aria-components';
import {Dialog} from './Dialog';
import {Menu, MenuItem, MenuTrigger} from './Menu';
import {Modal} from './Modal';
import plants, {Plant} from '@react-spectrum/docs/pages/react-aria/home/plants';
import {Popover} from './Popover';
import React, {useState} from 'react';
import {SearchField} from './SearchField';
import {Tag, TagGroup} from './TagGroup';
import {Tooltip} from './Tooltip';
import {useCollator, useFilter} from 'react-aria';
import {getSunlight, sunIcons, wateringIcons} from './Labels';
import {PlantTable} from './PlantTable';
import {PlantDialog} from './PlantDialog';
import {PlantList} from './PlantList';
export function App(): React.ReactNode {
  let [allItems, setAllItems] = useState<Plant[]>(() => plants.map(p => ({...p, isFavorite: false})));
  let [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
    column: 'common_name',
    direction: 'ascending'
  });
  let [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(['favorite', 'common_name', 'sunlight', 'watering', 'actions']));
  // Filter state.
  let [search, setSearch] = useState('');
  let [favorite, setFavorite] = useState(false);
  let [cycles, setCycles] = useState<Selection>(new Set());
  let [sunlight, setSunlight] = useState<Selection>(new Set());
  let [watering, setWatering] = useState<Selection>(new Set());
  // Filter and sort items.
  let {contains} = useFilter({sensitivity: 'base'});
  let collator = useCollator();
  let dir = sortDescriptor.direction === 'descending' ? -1 : 1;
  let items = allItems
    .filter(item =>
      (contains(item.common_name, search) || contains(item.scientific_name.join(''), search))
        && (!favorite || item.isFavorite)
        && (cycles === 'all' || cycles.size === 0 || cycles.has(item.cycle))
        && (sunlight === 'all' || sunlight.size === 0 || sunlight.has(getSunlight(item)))
        && (watering === 'all' || watering.size === 0 || watering.has(item.watering))
    )
    .sort((a: any, b: any) => collator.compare(a[sortDescriptor.column!], b[sortDescriptor.column!]) * dir);
  // Count applied filters for button badge.
  let filters = 0;
  if (favorite) {
    filters++;
  }
  if (cycles !== 'all') {
    filters += cycles.size;
  }
  if (sunlight !== 'all') {
    filters += sunlight.size;
  }
  if (watering !== 'all') {
    filters += watering.size;
  }
  let clearFilters = () => {
    setFavorite(false);
    setCycles(new Set());
    setSunlight(new Set());
    setWatering(new Set());
  };
  // Toggle whether an item is a favorite.
  let onFavoriteChange = (id: number, isFavorite: boolean) => {
    setAllItems(allItems => {
      let items = [...allItems];
      let index = items.findIndex(item => item.id === id);
      items[index] = {...items[index], isFavorite};
      return items;
    });
  };
  // Add, edit, and delete items.
  let addItem = (item: Plant) => {
    setAllItems(allItems => [...allItems, item]);
  };
  let editItem = (item: Plant) => {
    setAllItems(allItems => {
      let items = [...allItems];
      let index = items.findIndex(i => i.id === item.id);
      items[index] = item;
      return items;
    });
  };
  let deleteItem = () => {
    setAllItems(allItems => {
      if (!actionItem) {
        return allItems;
      }
      let items = [...allItems];
      let index = items.findIndex(item => item.id === actionItem!.id);
      items.splice(index, 1);
      return items;
    });
  };
  let [dialog, setDialog] = useState<Key | null>(null);
  let [actionItem, setActionItem] = useState<Plant | null>(null);
  let onEdit = (item: Plant) => {
    setDialog('edit');
    setActionItem(item);
  };
  let onDelete = (item: Plant) => {
    setDialog('delete');
    setActionItem(item);
  };
  return (
    <div className="h-full flex flex-col gap-4 p-4 max-w-[600px] mx-auto">
      <div className="grid grid-cols-[1fr_auto_auto] sm:grid-cols-[1.1fr_auto_auto_1fr_auto] gap-2 items-end">
        <SearchField
          aria-label="Search"
          value={search}
          onChange={setSearch}
          className="col-span-3 sm:col-span-1" />
        {/* Filters */}
        <DialogTrigger>
          <TooltipTrigger>
            <Button aria-label="Filters" variant="secondary" className="w-9 h-9 shrink-0 p-0 px-2 relative">
              <FilterIcon aria-hidden className="block w-5 h-5" />
              {filters > 0 && <div className="absolute -top-2 -right-2 rounded-full h-4 aspect-square text-white text-xs bg-blue-600">{filters}</div>}
            </Button>
            <Tooltip>Filters</Tooltip>
          </TooltipTrigger>
          <Popover showArrow>
            <Dialog className="outline outline-0 p-4 max-h-[inherit] overflow-auto w-[350px]">
              <Heading slot="title" className="text-lg font-semibold m-0 mb-2">Filters</Heading>
              {filters > 0 && <Button onPress={clearFilters} variant="secondary" className="absolute top-4 right-4 py-1 px-2 text-xs">Clear</Button>}
              <div className="flex flex-col gap-4">
                <Checkbox isSelected={favorite} onChange={setFavorite}>Favorite</Checkbox>
                <TagGroup label="Cycle" selectionMode="multiple" selectedKeys={cycles} onSelectionChange={setCycles} escapeKeyBehavior="none">
                  <Tag id="Annual" color="green" textValue="Annual"><RefreshCw className="w-4 h-4 shrink-0" /> Annual</Tag>
                  <Tag id="Perennial" color="green" textValue="Perennial"><RefreshCw className="w-4 h-4 shrink-0" /> Perennial</Tag>
                </TagGroup>
                <TagGroup label="Sunlight" selectionMode="multiple" selectedKeys={sunlight} onSelectionChange={setSunlight} escapeKeyBehavior="none">
                  <Tag id="full sun" color="yellow" textValue="Full Sun">{sunIcons['full sun']} Full Sun</Tag>
                  <Tag id="part sun" color="yellow" textValue="Part Sun">{sunIcons['part sun']} Part Sun</Tag>
                  <Tag id="part shade" color="yellow" textValue="Part Shade">{sunIcons['part shade']} Part Shade</Tag>
                </TagGroup>
                <TagGroup label="Watering" selectionMode="multiple" selectedKeys={watering} onSelectionChange={setWatering} escapeKeyBehavior="none">
                  <Tag id="Frequent" color="blue" textValue="Frequent">{wateringIcons['Frequent']} Frequent</Tag>
                  <Tag id="Average" color="blue" textValue="Average">{wateringIcons['Average']} Average</Tag>
                  <Tag id="Minimum" color="blue" textValue="Minimum">{wateringIcons['Minimum']} Minimum</Tag>
                </TagGroup>
              </div>
            </Dialog>
          </Popover>
        </DialogTrigger>
        {/* Columns */}
        <MenuTrigger>
          <TooltipTrigger>
            <Button aria-label="Columns" variant="secondary" className="w-9 h-9 shrink-0 p-0 hidden sm:block">
              <SlidersIcon aria-hidden className="block w-5 h-5" />
            </Button>
            <Tooltip>Columns</Tooltip>
          </TooltipTrigger>
          <Menu selectionMode="multiple" selectedKeys={visibleColumns} onSelectionChange={setVisibleColumns}>
            <MenuItem id="common_name">Name</MenuItem>
            <MenuItem id="cycle">Cycle</MenuItem>
            <MenuItem id="sunlight">Sunlight</MenuItem>
            <MenuItem id="watering">Watering</MenuItem>
          </Menu>
        </MenuTrigger>
        <DialogTrigger>
          <Button aria-label="Add plant" variant="secondary" className="w-9 h-9 shrink-0 p-0 col-start-5">
            <PlusIcon aria-hidden className="block w-5 h-5" />
          </Button>
          <Modal>
            <PlantDialog onSave={addItem} />
          </Modal>
        </DialogTrigger>
      </div>
      {/* List view for mobile */}
      <PlantList
        items={items}
        onFavoriteChange={onFavoriteChange}
        onEdit={onEdit}
        onDelete={onDelete} />
      {/* Table view for desktop */}
      <PlantTable
        sortDescriptor={sortDescriptor}
        onSortChange={setSortDescriptor}
        visibleColumns={visibleColumns}
        items={items}
        onFavoriteChange={onFavoriteChange}
        onEdit={onEdit}
        onDelete={onDelete} />
      <Modal isOpen={dialog === 'delete'} onOpenChange={() => setDialog(null)}>
        <AlertDialog title="Delete Plant" variant="destructive" actionLabel="Delete" onAction={deleteItem}>
          Are you sure you want to delete "{actionItem?.common_name}"?
        </AlertDialog>
      </Modal>
      <Modal isOpen={dialog === 'edit'} onOpenChange={() => setDialog(null)}>
        <PlantDialog item={actionItem} onSave={editItem} />
      </Modal>
    </div>
  );
}