# @hiyve/react-rooms

Room management components for Hiyve — manage persistent rooms and discover live rooms in real-time, with fully customizable labels, icons, colors, and styles.

## Installation

```bash
npx @hiyve/cli login
npm install @hiyve/react-rooms
```

## Quick Start

```tsx
import { RoomsList } from '@hiyve/react-rooms';
import { HiyveProvider } from '@hiyve/react';

function Dashboard() {
  return (
    <HiyveProvider generateRoomToken={getToken}>
      <RoomsList
        userId="user-123"
        onStartRoom={(room) => navigate(`/room/${room.roomName}`)}
        onCreateRoom={(room) => console.log('Created:', room.alias)}
      />
    </HiyveProvider>
  );
}
```

## Features

- **Full CRUD** — Create, list, edit, and delete persistent rooms
- **Live room discovery** — Real-time active room list via SSE (Server-Sent Events)
- **Custom entity names** — Rebrand "Room" to "Workspace", "Session", "Channel", etc.
- **Search, sort, and filter** — Client-side search by name, sort by date/name, filter by features
- **Static and link-based rooms** — Support both name-based and URL-based room joining
- **Room features** — Password protection, waiting room, offline mode toggles
- **Render props** — Replace individual layout sections (search bar, grid, create button, empty state) while keeping orchestration logic
- **Fully customizable** — Labels, icons, colors, and styles all accept partial overrides
- **i18n-ready** — Every string is customizable via label props

## Components

### Stored Rooms

| Component | Description |
|-----------|-------------|
| `RoomsList` | Complete room management interface — displays rooms grid with create, edit, and delete |
| `RoomCard` | Individual room card with start, settings, copy link, and delete actions |
| `RoomSearchBar` | Search input with sort and filter dropdowns |
| `CreateRoomDialog` | Dialog form for creating a new room with feature toggles |
| `RoomSettingsDialog` | Dialog for editing room settings (password, waiting room, offline) |
| `DeleteConfirmDialog` | Confirmation dialog before deleting a room |

### Room Detail

| Component | Description |
|-----------|-------------|
| `RoomDetailHeader` | Banner card with gradient header, room title, category badge, stats row, and action buttons |

### Active Room Discovery

| Component | Description |
|-----------|-------------|
| `ActiveRoomsList` | Real-time list of live rooms — connects via SSE, auto-updates as rooms appear/disappear |
| `ActiveRoomCard` | Individual active room card with live indicator, owner info, and join button |

## Hook

| Hook | Description |
|------|-------------|
| `useRoomFilters` | Client-side search, sort, and filter state management for room arrays |

## RoomsList Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `userId` | `string` | — | **Required.** User ID for room ownership and API operations |
| `onStartRoom` | `(room: StoredRoom) => void` | — | Called when a room's start/join button is clicked |
| `onCreateRoom` | `(room: StoredRoom) => void` | — | Called after a new room is successfully created |
| `onDeleteRoom` | `(alias: string) => void` | — | Called after a room is successfully deleted |
| `onSelectRoom` | `(room: StoredRoom) => void` | — | Called when a room card is clicked |
| `labels` | `Partial<RoomsLabels>` | — | Combined labels for all sub-components |
| `icons` | `Partial<RoomsIcons>` | — | Combined icons for all sub-components |
| `colors` | `Partial<RoomsColors>` | — | Combined colors for all sub-components |
| `styles` | `Partial<RoomsStyles>` | — | Combined MUI sx styles for all sub-components |
| `showCreateButton` | `boolean` | `true` | Whether to show the create room button |
| `showSearchBar` | `boolean` | `true` | Whether to show the search/filter bar |
| `showStaticOption` | `boolean` | `true` | Whether to show the static room option in the create dialog |
| `emptyStateContent` | `ReactNode` | — | Custom content when there are no rooms |
| `headerContent` | `ReactNode` | — | Custom content in the header area |
| `onError` | `(error: Error) => void` | — | Callback when an error occurs during any operation |
| `renderProps` | `RoomsListRenderProps` | — | Render props for replacing individual layout sections |

## Entity Labels (Rebranding)

The core customization point. Set entity labels once, and all components update their text accordingly.

```tsx
import { RoomsList } from '@hiyve/react-rooms';

// Rebrand as workspaces
<RoomsList
  userId="user-1"
  labels={{
    entity: {
      entity: 'Workspace',
      entityPlural: 'Workspaces',
      createEntity: 'Create Workspace',
      noEntities: 'No workspaces yet',
      deleteEntity: 'Delete Workspace',
      editEntity: 'Edit Workspace',
      searchEntities: 'Search workspaces',
    },
  }}
  onStartRoom={(room) => navigate(`/workspace/${room.roomName}`)}
/>

// Rebrand as sessions
<RoomsList
  userId="host-1"
  labels={{
    entity: {
      entity: 'Session',
      entityPlural: 'Sessions',
      createEntity: 'Schedule Session',
      noEntities: 'No sessions scheduled',
    },
  }}
/>
```

## Customization

Every component supports customization through `labels`, `icons`, `colors`, and `styles` props. Pass a partial object to override only the values you need — unspecified keys use defaults.

### Labels

```tsx
import { RoomsList } from '@hiyve/react-rooms';

<RoomsList
  userId="user-123"
  labels={{
    entity: { entity: 'Meeting', entityPlural: 'Meetings' },
    card: { startButton: 'Join Now', linkCopied: 'Copied!' },
    searchBar: { searchPlaceholder: 'Find meetings...' },
    createDialog: { title: 'New Meeting', createButton: 'Schedule' },
    settingsDialog: { title: 'Meeting Options', saveButton: 'Update' },
  }}
/>
```

### Colors

```tsx
import { RoomsList } from '@hiyve/react-rooms';

// Dark theme
<RoomsList
  userId="user-123"
  colors={{
    card: {
      background: '#1e1e1e',
      border: '#333',
      hoverBackground: '#2a2a2a',
      headerBackground: '#252525',
    },
  }}
/>
```

### Icons

```tsx
import { RoomsList } from '@hiyve/react-rooms';
import { Rocket, Trash2, Cog } from 'lucide-react';

<RoomsList
  userId="user-123"
  icons={{
    card: {
      startIcon: <Rocket size={18} />,
      deleteIcon: <Trash2 size={18} />,
      settingsIcon: <Cog size={18} />,
    },
    searchBar: {
      searchIcon: <Search size={18} />,
    },
  }}
/>
```

### Styles (MUI sx)

```tsx
import { RoomsList } from '@hiyve/react-rooms';

<RoomsList
  userId="user-123"
  styles={{
    card: {
      card: { borderRadius: 3, boxShadow: 2 },
      header: { py: 1.5 },
      actions: { justifyContent: 'flex-start' },
    },
  }}
/>
```

### Custom Empty State

```tsx
import { RoomsList } from '@hiyve/react-rooms';
import { Box, Typography } from '@mui/material';

<RoomsList
  userId="user-123"
  emptyStateContent={
    <Box textAlign="center" py={6}>
      <Typography variant="h6">Welcome!</Typography>
      <Typography color="text.secondary">
        Create your first room to get started.
      </Typography>
    </Box>
  }
/>
```

## Render Props

Replace individual layout sections while keeping all orchestration logic (data fetching, dialog management, CRUD handlers) intact. Each render function receives the relevant state and action handlers.

### Available Render Props

| Render Prop | Replaces | Data Provided |
|-------------|----------|---------------|
| `renderSearchBar` | Default search/sort/filter bar | Search query, sort/filter state and setters, `clearFilters`, `activeFiltersCount` |
| `renderGrid` | Default card grid | Filtered rooms array, selection state, `onStart`, `onSelect`, `onSettings`, `onDelete` |
| `renderCreateButton` | Default "Create Room" button | `onCreate` handler, entity labels |
| `renderEmptyState` | Default empty state | `onCreate` handler, `isLoading`, entity labels, `showCreateButton` |

Dialogs (create, settings, delete) are always managed internally — render props only affect layout sections.

### List Layout

```tsx
import { RoomsList } from '@hiyve/react-rooms';
import { List, ListItem, ListItemText, IconButton } from '@mui/material';
import { PlayArrow, Settings, Delete } from '@mui/icons-material';

<RoomsList
  userId="user-123"
  onStartRoom={handleStart}
  renderProps={{
    renderGrid: ({ rooms, onStart, onSelect, onSettings, onDelete }) => (
      <List>
        {rooms.map((room) => (
          <ListItem
            key={room.alias}
            onClick={() => onSelect(room)}
            secondaryAction={
              <>
                <IconButton onClick={() => onSettings(room)}><Settings /></IconButton>
                <IconButton onClick={() => onDelete(room)}><Delete /></IconButton>
              </>
            }
          >
            <ListItemText primary={room.alias} secondary={room.roomName} />
            <IconButton onClick={() => onStart(room)}><PlayArrow /></IconButton>
          </ListItem>
        ))}
      </List>
    ),
  }}
/>
```

### Floating Action Button

```tsx
import { Fab } from '@mui/material';
import { Add } from '@mui/icons-material';

<RoomsList
  userId="user-123"
  renderProps={{
    renderCreateButton: ({ onCreate, entityLabels }) => (
      <Fab color="primary" onClick={onCreate} sx={{ position: 'fixed', bottom: 24, right: 24 }}>
        <Add />
      </Fab>
    ),
  }}
/>
```

### Custom Search

```tsx
import { RoomsList } from '@hiyve/react-rooms';
import { TextField } from '@mui/material';

<RoomsList
  userId="user-123"
  renderProps={{
    renderSearchBar: ({ searchQuery, setSearchQuery, totalRooms }) => (
      <TextField
        fullWidth
        placeholder={`Search ${totalRooms} rooms...`}
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
      />
    ),
  }}
/>
```

### Precedence

- `renderSearchBar` ignores the `showSearchBar` boolean (still gated by `rooms.length > 0`)
- `renderCreateButton` ignores the `showCreateButton` boolean
- `renderEmptyState` ignores the `emptyStateContent` ReactNode
- `renderGrid` replaces the entire default grid

## RoomDetailHeader

A banner card for displaying room details with a gradient header, title, optional category badge, stats row, and action buttons. Suitable for room detail pages or dashboards.

```tsx
import { RoomDetailHeader } from '@hiyve/react-rooms';
import GroupIcon from '@mui/icons-material/Group';
import FolderIcon from '@mui/icons-material/Folder';
import { Button } from '@mui/material';

function RoomDetail({ room }) {
  return (
    <RoomDetailHeader
      room={room}
      headerGradient={['#5d6b82', '#3d4a5c']}
      categoryBadge={{ label: 'Team' }}
      stats={[
        { icon: <GroupIcon fontSize="small" />, label: 'Members', value: '12' },
        { icon: <FolderIcon fontSize="small" />, label: 'Files', value: '45' },
      ]}
      actions={<Button variant="contained">Start</Button>}
    />
  );
}
```

### RoomDetailHeader Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `room` | `StoredRoom` | _(required)_ | Room data (displays `alias` or `roomName` as title, reads `metadata.description`) |
| `headerGradient` | `[string, string]` | `['#5d6b82', '#3d4a5c']` | Gradient colors for the header banner `[startColor, endColor]` |
| `headerIcon` | `ReactNode` | -- | Watermark icon rendered centered in the banner |
| `categoryBadge` | `{ label: string; icon?: ReactNode }` | -- | Category badge shown in the banner |
| `stats` | `RoomDetailStat[]` | -- | Stats to display below the banner |
| `actions` | `ReactNode` | -- | Action buttons rendered in the banner |
| `isMobile` | `boolean` | `false` | Whether to use mobile layout (smaller title variant) |
| `labels` | `Partial<RoomDetailHeaderLabels>` | -- | Custom labels |
| `sx` | `SxProps<Theme>` | -- | MUI sx prop |
| `onError` | `(error: Error) => void` | -- | Error callback |

### RoomDetailStat

| Property | Type | Description |
|----------|------|-------------|
| `icon` | `ReactNode` | Icon for the stat tile |
| `label` | `string` | Stat label |
| `value` | `string` | Stat value |

## Active Room Discovery

Guests can discover and join live rooms in real-time using **targeted discovery**. Room owners advertise their rooms via `@hiyve/cloud` and specify which users can see them. Only targeted users receive room events through the SSE stream.

Every advertised room requires a `targetUserIds` array — there is no broadcast mode. This ensures rooms are only visible to their intended audience.

### Quick Start

```tsx
import { ActiveRoomsList } from '@hiyve/react-rooms';
import { HiyveProvider } from '@hiyve/react';
import { CloudClient } from '@hiyve/cloud';

const cloudClient = new CloudClient({ cloudToken: 'ct_your_token' });
const userId = 'guest-1';

function Lobby() {
  const [streamUrl, setStreamUrl] = useState<string>();

  useEffect(() => {
    cloudClient.getActiveRoomsStreamUrl(userId).then(setStreamUrl);
  }, []);

  if (!streamUrl) return null;

  return (
    <HiyveProvider generateRoomToken={getToken}>
      <ActiveRoomsList
        streamUrl={streamUrl}
        onJoinRoom={(room) => store.joinRoom(room.name, userId)}
      />
    </HiyveProvider>
  );
}
```

### Owner: Advertise a Room

```tsx
import { CloudClient } from '@hiyve/cloud';

const cloudClient = new CloudClient({ cloudToken: 'ct_your_token' });

// After creating a room, advertise it to specific users
await store.createRoom('Team Standup', userId);
await cloudClient.advertiseRoom({
  name: 'Team Standup',
  ownerDisplayName: 'Karl',
  targetUserIds: ['guest-1', 'guest-2', 'guest-3'],
  metadata: { waitingRoom: true },
});

// Update who can see the room (e.g., add a late invitee)
await cloudClient.updateAdvertisedRoom('Team Standup', {
  targetUserIds: ['guest-1', 'guest-2', 'guest-3', 'guest-4'],
});

// When leaving, remove from discovery
await store.leaveRoom();
await cloudClient.removeAdvertisedRoom('Team Standup');
```

### One-Shot Fetch

Use `getActiveRooms` for a one-time query instead of a live stream:

```tsx
// Fetch rooms targeted at a specific user
const rooms = await cloudClient.getActiveRooms(userId);

// Or fetch all rooms for the org (no userId filter)
const allRooms = await cloudClient.getActiveRooms();
```

### ActiveRoomsList Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `streamUrl` | `string` | — | **Required.** SSE stream URL from `await cloudClient.getActiveRoomsStreamUrl(userId)` |
| `onJoinRoom` | `(room: ActiveRoom) => void` | — | Called when a room's join button is clicked |
| `labels` | `Partial<ActiveRoomsListLabels>` | — | Labels for the list (title, empty state, search placeholder) |
| `cardLabels` | `Partial<ActiveRoomCardLabels>` | — | Labels passed to each card |
| `cardColors` | `Partial<ActiveRoomCardColors>` | — | Colors passed to each card |
| `cardStyles` | `Partial<ActiveRoomCardStyles>` | — | MUI sx styles passed to each card |
| `cardIcons` | `Partial<ActiveRoomCardIcons>` | — | Icons passed to each card |
| `showSearch` | `boolean` | `true` | Whether to show the search bar |
| `showTitle` | `boolean` | `true` | Whether to show the title |
| `emptyStateContent` | `ReactNode` | — | Custom content when there are no active rooms |
| `onError` | `(error: Error) => void` | — | Callback when an error occurs |
| `renderCard` | `(room, onJoin) => ReactNode` | — | Custom render function for each card |

### ActiveRoomCard Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `room` | `ActiveRoom` | — | **Required.** The active room to display |
| `onJoin` | `(room: ActiveRoom) => void` | — | Called when the join button is clicked |
| `labels` | `Partial<ActiveRoomCardLabels>` | — | Custom labels (join button, live indicator, hosted-by prefix) |
| `colors` | `Partial<ActiveRoomCardColors>` | — | Custom colors (background, border, live indicator) |
| `styles` | `Partial<ActiveRoomCardStyles>` | — | MUI sx styles for card, content, and actions |
| `icons` | `Partial<ActiveRoomCardIcons>` | — | Custom icons (join button icon) |
| `onError` | `(error: Error) => void` | — | Error callback |

### Custom Card Rendering

```tsx
import { ActiveRoomsList } from '@hiyve/react-rooms';
import { Button, Typography, Paper } from '@mui/material';

<ActiveRoomsList
  streamUrl={streamUrl}
  onJoinRoom={handleJoin}
  renderCard={(room, onJoin) => (
    <Paper sx={{ p: 2 }}>
      <Typography variant="h6">{room.name}</Typography>
      <Typography color="text.secondary">
        by {room.ownerDisplayName || room.owner}
      </Typography>
      <Button onClick={() => onJoin?.(room)}>Join</Button>
    </Paper>
  )}
/>
```

### Customizing Labels

```tsx
<ActiveRoomsList
  streamUrl={streamUrl}
  onJoinRoom={handleJoin}
  labels={{
    title: 'Live Sessions',
    emptyState: 'No sessions running right now',
    searchPlaceholder: 'Find a session...',
  }}
  cardLabels={{
    joinButton: 'Enter',
    liveIndicator: 'In Progress',
    hostedBy: 'Led by',
  }}
/>
```

### Active Room Discovery Defaults

| Default | camelCase Alias | Merge Function |
|---------|----------------|---------------|
| `DEFAULT_ACTIVE_CARD_LABELS` | `defaultActiveCardLabels` | `mergeActiveCardLabels` |
| `DEFAULT_ACTIVE_CARD_COLORS` | `defaultActiveCardColors` | `mergeActiveCardColors` |
| `DEFAULT_ACTIVE_CARD_ICONS` | `defaultActiveCardIcons` | `mergeActiveCardIcons` |
| `DEFAULT_ACTIVE_LIST_LABELS` | `defaultActiveListLabels` | `mergeActiveListLabels` |

## Standalone Components

All components can be used individually for custom layouts:

```tsx
import {
  RoomCard,
  RoomSearchBar,
  CreateRoomDialog,
  useRoomFilters,
} from '@hiyve/react-rooms';
import { useStoredRooms } from '@hiyve/react';

function CustomRoomGrid() {
  const { rooms, fetchStoredRooms, addStoredRoom } = useStoredRooms();
  const { searchQuery, setSearchQuery, sortBy, setSortBy,
    filterBy, setFilterBy, processedRooms, clearFilters,
    activeFiltersCount } = useRoomFilters(rooms);
  const [createOpen, setCreateOpen] = useState(false);

  useEffect(() => { fetchStoredRooms('user-123'); }, []);

  return (
    <>
      <RoomSearchBar
        searchQuery={searchQuery}
        sortBy={sortBy}
        filterBy={filterBy}
        onSearchChange={setSearchQuery}
        onSortChange={setSortBy}
        onFilterChange={setFilterBy}
        onClear={clearFilters}
        activeFiltersCount={activeFiltersCount}
      />
      {processedRooms.map((room) => (
        <RoomCard
          key={room.id}
          room={room}
          onStart={(r) => joinRoom(r.roomName)}
        />
      ))}
      <CreateRoomDialog
        open={createOpen}
        onClose={() => setCreateOpen(false)}
        onSave={async (opts) => {
          await addStoredRoom(opts, 'user-123');
          setCreateOpen(false);
        }}
      />
    </>
  );
}
```

## Defaults and Merge Functions

All defaults are exported for consumers who want to extend or reference them:

**Entity Labels:**

| Default | camelCase Alias | Merge Function | Factory |
|---------|----------------|---------------|---------|
| `DEFAULT_ENTITY_LABELS` | `defaultEntityLabels` | `mergeEntityLabels` | -- |

**Component Labels:**

| Default | camelCase Alias | Merge Function | Factory |
|---------|----------------|---------------|---------|
| `DEFAULT_SEARCH_BAR_LABELS` | `defaultSearchBarLabels` | `mergeSearchBarLabels` | `createDefaultSearchBarLabels` |
| `DEFAULT_CREATE_DIALOG_LABELS` | `defaultCreateDialogLabels` | `mergeCreateDialogLabels` | `createDefaultCreateDialogLabels` |
| `DEFAULT_SETTINGS_DIALOG_LABELS` | `defaultSettingsDialogLabels` | `mergeSettingsDialogLabels` | `createDefaultSettingsDialogLabels` |
| `DEFAULT_CARD_LABELS` | `defaultCardLabels` | `mergeCardLabels` | `createDefaultCardLabels` |

**Icons and Colors:**

| Default | camelCase Alias | Merge Function |
|---------|----------------|---------------|
| `DEFAULT_CARD_COLORS` | `defaultCardColors` | `mergeCardColors` |
| `DEFAULT_CARD_ICONS` | `defaultCardIcons` | `mergeCardIcons` |
| `DEFAULT_SEARCH_BAR_ICONS` | `defaultSearchBarIcons` | `mergeSearchBarIcons` |

**Room Detail Header:**

| Default | camelCase Alias | Merge Function |
|---------|----------------|---------------|
| `DEFAULT_ROOM_DETAIL_HEADER_LABELS` | `defaultRoomDetailHeaderLabels` | `mergeRoomDetailHeaderLabels` |

Factory functions accept entity labels and generate defaults dynamically -- so `createDefaultCardLabels({ entity: 'Workspace', ... })` produces `startButton: 'Start Workspace'` automatically.

Both `SCREAMING_SNAKE_CASE` and `camelCase` aliases reference the same objects. Use whichever naming convention matches your codebase.

## Static vs Link-Based Rooms

Rooms support two join modes:

- **Link-based** (default) — Rooms get a generated join URL. Users join via the link.
- **Static** — Rooms use a fixed name. Users join by entering the room name directly.

```tsx
// Show the static room option in the create dialog
<RoomsList
  userId="user-123"
  showStaticOption={true}
  onStartRoom={(room) => {
    if (room.roomLink) {
      // Link-based: navigate to join URL
      window.location.href = room.roomLink;
    } else {
      // Static: join by name
      joinRoom(room.roomName);
    }
  }}
/>
```

Static rooms display a "Static" badge chip, while link-based rooms show a "Link" badge. The "Copy Link" action is only available for link-based rooms.

## Requirements

- **`@hiyve/react`** (`^2.0.0`) — components must be rendered inside `HiyveProvider`
- **`@hiyve/core`** (`^1.0.0`) — state store types and `StoredRoom` interface
- **`@hiyve/cloud`** (`^1.0.0`) — required for active room discovery (`CloudClient`)
- **`@hiyve/rtc-client`** — WebRTC client (provided by HiyveProvider)
- **`@hiyve/utilities`** (`^1.0.0`)
- **`@mui/material`** (`^5.0.0 || ^6.0.0`) and **`@mui/icons-material`**
- **`@emotion/react`** (`>=11`) and **`@emotion/styled`** (`>=11`)
- **`react`** (`^18.0.0`)

## License

Proprietary - Hiyve SDK
