feat(frontend): preview an inventory item as a customer sees it (#89)
Clicking an item's name in the admin Inventory opens a drawer rendering the real storefront ItemCard for it, so the way a listing will look can be checked without publishing it and going to see. The name cell is a link-styled button rather than a clickable cell, so it stays reachable by keyboard and announces itself as an action. Admin already imports Item from ../api, the same type the storefront uses, so the row object goes straight into the card with no adapter and nothing to drift. The part that needed care is that ItemCard is not a passive component. It wires into the cart and favorites contexts and has working buttons, and both providers wrap the whole app — so a naive preview would have been fully functional, and an admin browsing inventory could have added their own stock to their own cart. On a one-of-a-kind catalogue that reserves the item and takes it off sale. ItemCard therefore takes an optional preview prop that short-circuits its two click handlers. Those two are the only entry points, so guarding them also covers the shared auth modal and the favorite-alerts consent prompt hanging off them. Deliberately not `disabled` on the buttons. A disabled antd button renders in a different colour with a different cursor and no hover, and the whole point of this panel is to show what a customer will actually see. The controls keep their normal appearance and their correct state for the item's status; only the handlers stop. The comment on the prop says so, because "simplifying" this to a disabled button would quietly defeat the feature while appearing to implement it. Four end-to-end tests, two of which are the ones worth having. Clicking Add to Cart in the preview must do nothing — asserted by the sign-in prompt never appearing, which a real click on a signed-out card always raises, so its absence proves the handler stopped before doing any work. And the storefront card must still be live where it is actually used, or this change would have quietly broken buying things. ItemCard's props are now Readonly, which was an existing lint warning on a file this change already touches: frontend warnings drop from 31 to 30. Verified: build clean, lint 0 errors, 91 end-to-end tests passing against a freshly created database. Refs #89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Layout, Table, Button, Form, Input, InputNumber, Upload, Modal,
|
||||
Layout, Table, Button, Drawer, Form, Input, InputNumber, Upload, Modal,
|
||||
Space, Tag, Typography, Switch, message, Image as AntImage, theme, Tabs,
|
||||
Select
|
||||
} from 'antd';
|
||||
@@ -20,6 +20,7 @@ import Settings from './Settings';
|
||||
import Categories from './Categories';
|
||||
import Tags from './Tags';
|
||||
import CategoryTreeSelect from './CategoryTreeSelect';
|
||||
import ItemCard from '../components/ItemCard';
|
||||
import InventoryFilters from './InventoryFilters';
|
||||
import { ItemFilters, EMPTY_FILTERS } from '../filters';
|
||||
|
||||
@@ -34,6 +35,10 @@ function Inventory() {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<Item | null>(null);
|
||||
// The item whose storefront appearance is being previewed, or null when the
|
||||
// panel is closed. Holds the row object itself — admin and storefront share
|
||||
// one Item type, so there is nothing to convert and nothing to drift.
|
||||
const [previewItem, setPreviewItem] = useState<Item | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [description, setDescription] = useState<string>('');
|
||||
@@ -181,7 +186,17 @@ function Inventory() {
|
||||
</span>
|
||||
) : null
|
||||
},
|
||||
{ title: 'Name', dataIndex: 'name' },
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
// A button rather than a clickable cell so it is reachable by keyboard
|
||||
// and announces itself as an action.
|
||||
render: (name: string, item: Item) => (
|
||||
<Button type="link" style={{ padding: 0, height: 'auto', textAlign: 'left' }} onClick={() => setPreviewItem(item)}>
|
||||
{name}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'category_name',
|
||||
@@ -283,6 +298,26 @@ function Inventory() {
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* The real storefront card, rendered inert. Width is pinned to what the
|
||||
storefront grid actually gives a card at its widest column, so the
|
||||
proportions here match what a customer sees rather than stretching to
|
||||
fill the drawer. */}
|
||||
<Drawer
|
||||
title={previewItem ? `Preview: ${previewItem.name}` : 'Preview'}
|
||||
open={previewItem !== null}
|
||||
onClose={() => setPreviewItem(null)}
|
||||
width={420}
|
||||
destroyOnHidden
|
||||
>
|
||||
{previewItem && (
|
||||
<div style={{ maxWidth: 340, margin: '0 auto' }}>
|
||||
{/* onChanged never fires: every handler that would call it is
|
||||
short-circuited by `preview`. */}
|
||||
<ItemCard item={previewItem} onChanged={() => undefined} preview />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user