144 lines
5.7 KiB
Markdown
144 lines
5.7 KiB
Markdown
# Tier 3: Garden empty-state component, toggle-group for notify channels
|
|
|
|
## Goal
|
|
|
|
Create a reusable EmptyState component for garden (and potentially other modules), and replace manual checkbox patterns with shadcn ToggleGroup for notification channel selection.
|
|
|
|
---
|
|
|
|
### Task 3a: Garden empty-state component
|
|
|
|
**New file:** `src/modules/garden/components/empty-state.tsx`
|
|
|
|
**Purpose:** A shared empty-state component for the garden module — replacing ad-hoc "No plants yet." / "No containers yet." paragraphs scattered across garden list/detail pages.
|
|
|
|
**Design (base on shadcn's `<EmptyState>` from `src/components/ui/empty.tsx`):**
|
|
|
|
The empty component already exists at `src/components/ui/empty.tsx` — it provides a generic EmptyState wrapper with icon + title + description slots. We should **use it directly**, not create a garden-specific one, unless the garden needs garden-themed defaults.
|
|
|
|
**Garden-specific variants needed:**
|
|
|
|
- "No plants yet" — icon: leaf/plant, title: "No plants yet", description: "Add your first plant to get started."
|
|
- "No containers yet" — icon: box/pot, title: "No containers yet", description: "Add a container to group your plants."
|
|
|
|
**Files to update:**
|
|
|
|
1. `src/modules/garden/components/plant-list.tsx` line 22: `<p className="text-sm text-[var(--ink-mute)]">No plants yet.</p>` → `<EmptyState ... />`
|
|
2. `src/modules/garden/components/container-list.tsx` line 39: `"No containers yet. Add one to start organising your plants."` → `<EmptyState ... />`
|
|
3. `src/modules/garden/components/container-detail.tsx` line 169: `<p className="text-sm text-[var(--ink-mute)]">No plants in this container yet.</p>` → inline EmptyState or keep as brief fallback (it's a sub-section, not a full page)
|
|
|
|
**Import:**
|
|
|
|
```tsx
|
|
import { EmptyState } from "@/components/ui/empty"
|
|
import { Sprout, Container } from "lucide-react"
|
|
|
|
// In plant-list:
|
|
<EmptyState
|
|
icon={Sprout}
|
|
title="No plants yet"
|
|
description="Add your first plant to get started."
|
|
/>
|
|
|
|
// In container-list:
|
|
<EmptyState
|
|
icon={Container}
|
|
title="No containers yet"
|
|
description="Add a container to start organising your plants."
|
|
/>
|
|
```
|
|
|
|
**Acceptance for 3a:**
|
|
|
|
- EmptyState imported from `@/components/ui/empty` in all garden list pages
|
|
- Zero ad-hoc empty text paragraphs remain in garden component files (check plant-list, container-list, and any other garden page)
|
|
- Garden empty states are visually consistent (same icon size, title weight, description color)
|
|
|
|
---
|
|
|
|
### Task 3b: Toggle-group for notify channels
|
|
|
|
**File:** `src/components/notify-channel-toggles.tsx`
|
|
|
|
**Current state:** Each channel is a native `<input type="checkbox">` wrapped in a label with custom styling. No grouping or visual cohesion.
|
|
|
|
**Change to ToggleGroup + ToggleGroupItem:**
|
|
|
|
```tsx
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
|
|
|
export function NotifyChannelToggles({
|
|
push, inapp, ntfy, ntfyConfigured,
|
|
}: { ... }) {
|
|
const channels: { key: Channel; value: boolean; label: string; disabled?: boolean }[] = [
|
|
{ key: "push", value: push, label: "Web push" },
|
|
{ key: "inapp", value: inapp, label: "In-app inbox" },
|
|
{ key: "ntfy", value: ntfy, label: "ntfy", disabled: !ntfyConfigured },
|
|
];
|
|
|
|
const enabledKeys = channels.filter(c => c.value && !c.disabled).map(c => c.key);
|
|
|
|
return (
|
|
<ToggleGroup type="multiple" defaultValue={enabledKeys} onValueChange={(v) => handleToggle(v)}>
|
|
{channels.map(({ key, value, label, disabled }) => (
|
|
<ToggleGroupItem
|
|
key={key}
|
|
value={key}
|
|
disabled={disabled || !("PushManager" in window) && key === "push"}
|
|
aria-label={label}
|
|
>
|
|
{label}
|
|
</ToggleGroupItem>
|
|
))}
|
|
</ToggleGroup>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Important:** The ToggleGroup needs to integrate with the existing server action flow (`setNotifChannel`). The `onValueChange` handler should:
|
|
|
|
1. Detect which channels were added/removed from the selection
|
|
2. Call `setNotifChannel(channel, enabled)` for each change
|
|
3. Still trigger an optimistic UI update
|
|
|
|
**Alternative approach** (safer — use individual ToggleGroupItems as independent switches): Since these are truly independent toggles (enabling push doesn't require disabling inapp), we keep the current per-channel rendering but swap `<input type="checkbox">` to shadcn's `<Switch>` component from `@/components/ui/switch`:
|
|
|
|
```tsx
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
// Each channel:
|
|
<label className="flex items-center justify-between gap-4">
|
|
<span className="text-sm">{label}</span>
|
|
<Switch
|
|
checked={value}
|
|
disabled={disabled || isPending}
|
|
onCheckedChange={(v) => toggle(key, v)}
|
|
/>
|
|
</label>;
|
|
```
|
|
|
|
**Decision:** The `<Switch>` approach is more appropriate here. ToggleGroup is for mutually-exclusive selection; Switch is for independent on/off toggles — which matches the notification channels use case perfectly. **Use `Switch` from shadcn, not ToggleGroup.**
|
|
|
|
**Acceptance for 3b:**
|
|
|
|
- Each channel uses shadcn `<Switch>` component (not native checkbox)
|
|
- Visual appearance matches the app's design system
|
|
- Server action integration works correctly (toggle enables/disables on the server)
|
|
- Disabled state handled properly for ntfy when unconfigured
|
|
|
|
---
|
|
|
|
## Acceptance criteria
|
|
|
|
1. EmptyState component from `@/components/ui/empty` used in all garden list pages
|
|
2. All ad-hoc "No plants yet." / "No containers yet." text replaced with EmptyState or kept as contextual inline text where appropriate
|
|
3. Notification channels use shadcn `<Switch>` components
|
|
4. Visual consistency across garden empty states
|
|
|
|
## Steps
|
|
|
|
1. Use existing `src/components/ui/empty.tsx` EmptyState in plant-list.tsx and container-list.tsx
|
|
2. Replace Switch component imports in notify-channel-toggles.tsx
|
|
3. Verify server action integration for toggles
|
|
4. Run `pnpm lint` and `pnpm typecheck`
|