SkateSpots/src/hooks/useMarkListingSold.ts
shakespeare.diy 080785a8c5 Marketplace: give sellers real listing management (mark sold / relist / delete) (1.2.30)
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>
2026-09-07 22:24:47 +07:00

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 };
}