Gap found: the app's data model, sales tracking (useSellerSales), and Stash
showcase all key off a listing's status='sold', and both listing cards render a
"sold" state — but there was NO way for a seller to ever set it. Owners had zero
management actions: the MarketplaceItemCard dropdown only rendered for
non-owners (Report), and GearSwapItemCard's owner branch literally said "owners
manage their listing elsewhere" — a place that didn't exist. So sold gear stayed
"active" forever (until 30-day expiry) and the whole sold/sales-count feature was
unreachable.
Added owner-facing management on BOTH listing cards (GearSwapItemCard drives the
main GearSwap grid; MarketplaceItemCard is used on spot pages):
- Mark as sold / Relist as active
- Delete listing (NIP-09, via the existing useDeleteEntity)
New useMarkListingSold hook flips status in place, PRESERVING every other tag
(title, price, images, size, spot ref, geohash, expiration, category t-tags) —
kind 30402 is addressable, so rebuilding from scratch would wipe the photos and
price. Same "preserve unmanaged tags" discipline as the recent event-edit fix.
Also:
- GearSwapItemCard now shows a clear diagonal "SOLD" overlay when sold, so the
status is actually visible to buyers and the owner (previously invisible).
- Generalized its admin-remove flow to handle owner deletes too, with wording
that adapts (owner "Delete" vs admin "Remove").
- DeleteEntityDialog gained an optional controlled open/onOpenChange so it can
be driven from inside a dropdown menu without unmounting on menu close.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
52 lines
2 KiB
TypeScript
52 lines
2 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { MARKETPLACE_ITEM_KIND } from '@/lib/types';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
/**
|
|
* Flip a NIP-99 gear listing's status (active ⇄ sold).
|
|
*
|
|
* A listing (kind 30402) is addressable, so re-publishing REPLACES the whole
|
|
* tag set. We therefore start from the original event's tags and change ONLY
|
|
* the `status` tag — every other tag (title, price, images, size, spot `a`
|
|
* ref, geohash, expiration, published_at, category `t` tags…) is carried
|
|
* through untouched. Rebuilding from scratch here would wipe the listing's
|
|
* photos and price, so we deliberately don't.
|
|
*/
|
|
export function useMarkListingSold() {
|
|
const { mutateAsync: createEvent, isPending } = useNostrPublish();
|
|
const { user } = useCurrentUser();
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutate = async ({
|
|
event,
|
|
status,
|
|
}: {
|
|
/** The original listing event. */
|
|
event: NostrEvent;
|
|
/** New status — 'sold' to mark sold, 'active' to relist. */
|
|
status: 'sold' | 'active';
|
|
}) => {
|
|
if (!user) throw new Error('You must be logged in.');
|
|
if (user.pubkey !== event.pubkey) throw new Error('You can only update your own listing.');
|
|
if (event.kind !== MARKETPLACE_ITEM_KIND) throw new Error('Not a gear listing.');
|
|
|
|
// Preserve every tag except `status`, then set the new status.
|
|
const tags = event.tags.filter(([name]) => name !== 'status');
|
|
tags.push(['status', status]);
|
|
|
|
await createEvent({
|
|
kind: MARKETPLACE_ITEM_KIND,
|
|
content: event.content,
|
|
tags,
|
|
});
|
|
|
|
// Refresh marketplace + seller views so the badge updates immediately.
|
|
queryClient.invalidateQueries();
|
|
};
|
|
|
|
return { markSold: (event: NostrEvent) => mutate({ event, status: 'sold' }),
|
|
relist: (event: NostrEvent) => mutate({ event, status: 'active' }),
|
|
isPending };
|
|
}
|