feat(21-03): remove slide-out panels from root layout and clean UIStore

- Remove Item and Candidate SlideOutPanel instances from __root.tsx
- Remove SlideOutPanel, ItemForm, CandidateForm imports from root
- Remove panel state (panelMode, editingItemId, candidatePanelMode, editingCandidateId) from UIStore
- Remove panel actions (openEditPanel, openAddPanel, closePanel, etc.) from UIStore
- Preserve currentThreadId derivation for CandidateDeleteDialog
- Update CollectionView empty state to use catalog search instead of add panel
- Update ItemForm/CandidateForm with onClose prop replacing store panel close
- Clean all dead panel references across src/client/
This commit is contained in:
2026-04-06 15:12:59 +02:00
parent 1f79c5ca3c
commit 4c79735426
6 changed files with 16 additions and 89 deletions

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import { useCreateCandidate, useUpdateCandidate } from "../hooks/useCandidates";
import { useThread } from "../hooks/useThreads";
import { useUIStore } from "../stores/uiStore";
import { CategoryPicker } from "./CategoryPicker";
import { ImageUpload } from "./ImageUpload";
@@ -9,6 +8,7 @@ interface CandidateFormProps {
mode: "add" | "edit";
threadId: number;
candidateId?: number | null;
onClose?: () => void;
}
interface FormData {
@@ -39,11 +39,11 @@ export function CandidateForm({
mode,
threadId,
candidateId,
onClose,
}: CandidateFormProps) {
const { data: thread } = useThread(threadId);
const createCandidate = useCreateCandidate(threadId);
const updateCandidate = useUpdateCandidate(threadId);
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
const [form, setForm] = useState<FormData>(INITIAL_FORM);
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -124,13 +124,13 @@ export function CandidateForm({
createCandidate.mutate(payload, {
onSuccess: () => {
setForm(INITIAL_FORM);
closeCandidatePanel();
onClose?.();
},
});
} else if (candidateId != null) {
updateCandidate.mutate(
{ candidateId, ...payload },
{ onSuccess: () => closeCandidatePanel() },
{ onSuccess: () => onClose?.() },
);
}
}

View File

@@ -14,7 +14,7 @@ export function CollectionView() {
const { data: totals } = useTotals();
const { data: categories } = useCategories();
const { weight, price } = useFormatters();
const openAddPanel = useUIStore((s) => s.openAddPanel);
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
const [searchText, setSearchText] = useState("");
const [categoryFilter, setCategoryFilter] = useState<number | null>(null);
@@ -66,7 +66,7 @@ export function CollectionView() {
</p>
<button
type="button"
onClick={openAddPanel}
onClick={() => openCatalogSearch("collection")}
className="inline-flex items-center gap-2 px-5 py-2.5 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg transition-colors"
>
<svg

View File

@@ -7,6 +7,7 @@ import { ImageUpload } from "./ImageUpload";
interface ItemFormProps {
mode: "add" | "edit";
itemId?: number | null;
onClose?: () => void;
}
interface FormData {
@@ -31,11 +32,10 @@ const INITIAL_FORM: FormData = {
imageFilename: null,
};
export function ItemForm({ mode, itemId }: ItemFormProps) {
export function ItemForm({ mode, itemId, onClose }: ItemFormProps) {
const { data: items } = useItems();
const createItem = useCreateItem();
const updateItem = useUpdateItem();
const closePanel = useUIStore((s) => s.closePanel);
const openConfirmDelete = useUIStore((s) => s.openConfirmDelete);
const [form, setForm] = useState<FormData>(INITIAL_FORM);
@@ -112,13 +112,13 @@ export function ItemForm({ mode, itemId }: ItemFormProps) {
createItem.mutate(payload, {
onSuccess: () => {
setForm(INITIAL_FORM);
closePanel();
onClose?.();
},
});
} else if (itemId != null) {
updateItem.mutate(
{ id: itemId, ...payload },
{ onSuccess: () => closePanel() },
{ onSuccess: () => onClose?.() },
);
}
}

View File

@@ -9,14 +9,11 @@ import {
} from "@tanstack/react-router";
import { useState } from "react";
import "../app.css";
import { CandidateForm } from "../components/CandidateForm";
import { CatalogSearchOverlay } from "../components/CatalogSearchOverlay";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { ExternalLinkDialog } from "../components/ExternalLinkDialog";
import { FabMenu } from "../components/FabMenu";
import { ItemForm } from "../components/ItemForm";
import { OnboardingWizard } from "../components/OnboardingWizard";
import { SlideOutPanel } from "../components/SlideOutPanel";
import { TotalsBar } from "../components/TotalsBar";
import { useAuth } from "../hooks/useAuth";
import { useDeleteCandidate } from "../hooks/useCandidates";
@@ -79,16 +76,6 @@ function RootLayout() {
const { data: auth, isLoading: authLoading } = useAuth();
const isAuthenticated = !!auth?.user;
// Item panel state
const panelMode = useUIStore((s) => s.panelMode);
const editingItemId = useUIStore((s) => s.editingItemId);
const closePanel = useUIStore((s) => s.closePanel);
// Candidate panel state
const candidatePanelMode = useUIStore((s) => s.candidatePanelMode);
const editingCandidateId = useUIStore((s) => s.editingCandidateId);
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
// Candidate delete state
const confirmDeleteCandidateId = useUIStore(
(s) => s.confirmDeleteCandidateId,
@@ -114,9 +101,6 @@ function RootLayout() {
!wizardDismissed &&
isAuthenticated;
const isItemPanelOpen = panelMode !== "closed";
const isCandidatePanelOpen = candidatePanelMode !== "closed";
// Route matching for contextual behavior
const matchRoute = useMatchRoute();
@@ -186,40 +170,6 @@ function RootLayout() {
<TotalsBar {...finalTotalsProps} />
<Outlet />
{/* Item Slide-out Panel */}
<SlideOutPanel
isOpen={isItemPanelOpen}
onClose={closePanel}
title={panelMode === "add" ? "Add Item" : "Edit Item"}
>
{panelMode === "add" && <ItemForm mode="add" />}
{panelMode === "edit" && (
<ItemForm mode="edit" itemId={editingItemId} />
)}
</SlideOutPanel>
{/* Candidate Slide-out Panel */}
{currentThreadId != null && (
<SlideOutPanel
isOpen={isCandidatePanelOpen}
onClose={closeCandidatePanel}
title={
candidatePanelMode === "add" ? "Add Candidate" : "Edit Candidate"
}
>
{candidatePanelMode === "add" && (
<CandidateForm mode="add" threadId={currentThreadId} />
)}
{candidatePanelMode === "edit" && (
<CandidateForm
mode="edit"
threadId={currentThreadId}
candidateId={editingCandidateId}
/>
)}
</SlideOutPanel>
)}
{/* Item Confirm Delete Dialog */}
<ConfirmDialog />

View File

@@ -23,7 +23,7 @@ function ThreadDetailPage() {
const { threadId: threadIdParam } = Route.useParams();
const threadId = Number(threadIdParam);
const { data: thread, isLoading, isError } = useThread(threadId);
const openCandidateAddPanel = useUIStore((s) => s.openCandidateAddPanel);
const [_showAddCandidate, setShowAddCandidate] = useState(false);
const candidateViewMode = useUIStore((s) => s.candidateViewMode);
const setCandidateViewMode = useUIStore((s) => s.setCandidateViewMode);
const selectedSetupId = useUIStore((s) => s.selectedSetupId);
@@ -132,7 +132,7 @@ function ThreadDetailPage() {
{isActive && (
<button
type="button"
onClick={openCandidateAddPanel}
onClick={() => setShowAddCandidate(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-800 text-white text-sm font-medium rounded-lg transition-colors"
>
<svg

View File

@@ -1,25 +1,15 @@
import { create } from "zustand";
interface UIState {
// Item panel state
panelMode: "closed" | "add" | "edit";
editingItemId: number | null;
// Item delete state
confirmDeleteItemId: number | null;
openAddPanel: () => void;
openEditPanel: (itemId: number) => void;
closePanel: () => void;
openConfirmDelete: (itemId: number) => void;
closeConfirmDelete: () => void;
// Candidate panel state
candidatePanelMode: "closed" | "add" | "edit";
editingCandidateId: number | null;
// Candidate delete state
confirmDeleteCandidateId: number | null;
openCandidateAddPanel: () => void;
openCandidateEditPanel: (id: number) => void;
closeCandidatePanel: () => void;
openConfirmDeleteCandidate: (id: number) => void;
closeConfirmDeleteCandidate: () => void;
@@ -70,28 +60,15 @@ interface UIState {
}
export const useUIStore = create<UIState>((set) => ({
// Item panel
panelMode: "closed",
editingItemId: null,
// Item delete
confirmDeleteItemId: null,
openAddPanel: () => set({ panelMode: "add", editingItemId: null }),
openEditPanel: (itemId) => set({ panelMode: "edit", editingItemId: itemId }),
closePanel: () => set({ panelMode: "closed", editingItemId: null }),
openConfirmDelete: (itemId) => set({ confirmDeleteItemId: itemId }),
closeConfirmDelete: () => set({ confirmDeleteItemId: null }),
// Candidate panel
candidatePanelMode: "closed",
editingCandidateId: null,
// Candidate delete
confirmDeleteCandidateId: null,
openCandidateAddPanel: () =>
set({ candidatePanelMode: "add", editingCandidateId: null }),
openCandidateEditPanel: (id) =>
set({ candidatePanelMode: "edit", editingCandidateId: id }),
closeCandidatePanel: () =>
set({ candidatePanelMode: "closed", editingCandidateId: null }),
openConfirmDeleteCandidate: (id) => set({ confirmDeleteCandidateId: id }),
closeConfirmDeleteCandidate: () => set({ confirmDeleteCandidateId: null }),