- labelsUi.* (221+ keys): spot categories/features/types, access, shelter, event types, gear categories/conditions, price/listings, tours, crews, member-visibility, tag categories, spot status. New src/lib/labelKeys.ts holds the shared value->key maps (one source; the five duplicated EVENT_TYPE_KEYS copies now import it). spotStatus.statusTimeAgo() uses the locale date shim. - formatPrice() now takes an optional t so FREE/Trade/Make Offer translate. - Route helpers: skateTime/distanceReadout/maneuver descriptors/formatAge translated (routeUi.*), consumers updated. - Sasquatch: PRO_TIERS labels/badges + summarizeDraft titles/lines/ctas translated (sasquatchUi.*). - Lab *_LABELS/_DESCRIPTIONS maps remain English (documented out of scope).
174 lines
7.4 KiB
TypeScript
174 lines
7.4 KiB
TypeScript
/**
|
|
* useSasquatchActions — publish the actions Sasquatch drafts (Pro).
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Sasquatch never publishes on its own. The AI proposes a structured DRAFT; the
|
|
* user reviews it in a confirmation card; only on confirm do we sign & publish
|
|
* here. All actions reuse the app's existing event schemas.
|
|
*
|
|
* Supported drafts:
|
|
* • post_status — NIP-38 skating status (kind 30315), optional expiry
|
|
* • list_gear — NIP-99 marketplace listing (kind 30402), skateboarding-tagged
|
|
*/
|
|
|
|
import { useCallback } from 'react';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { MARKETPLACE_ITEM_KIND } from '@/lib/types';
|
|
import { USER_STATUS_KIND } from '@/hooks/useUserStatus';
|
|
|
|
/** Minimal translation-function shape (this module is imported by components). */
|
|
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
|
|
|
// ─── Draft shapes ─────────────────────────────────────────────────────────────
|
|
|
|
export interface PostStatusDraft {
|
|
type: 'post_status';
|
|
/** Freeform status text, e.g. "🛹 Skating Southbank". */
|
|
text: string;
|
|
/** Minutes until it auto-expires (default 120). */
|
|
expiresInMinutes?: number;
|
|
}
|
|
|
|
export interface ListGearDraft {
|
|
type: 'list_gear';
|
|
title: string;
|
|
description?: string;
|
|
/** Price in the given currency (default sats). Omit for "make offer". */
|
|
price?: number;
|
|
currency?: string;
|
|
/** deck | trucks | wheels | bearings | shoes | clothing | other */
|
|
category?: string;
|
|
condition?: string;
|
|
size?: string;
|
|
}
|
|
|
|
/** "Wanted" ad — the skater is LOOKING FOR gear (NIP-99 listing_type=wtb). */
|
|
export interface WtbGearDraft {
|
|
type: 'wtb_gear';
|
|
/** What they're looking for. */
|
|
title: string;
|
|
description?: string;
|
|
/** Maximum sats they'll pay. Omit for "open". */
|
|
price?: number;
|
|
currency?: string;
|
|
category?: string;
|
|
condition?: string;
|
|
size?: string;
|
|
}
|
|
|
|
export type SasquatchDraft = PostStatusDraft | ListGearDraft | WtbGearDraft;
|
|
|
|
// ─── Human-readable summary for the confirmation card ────────────────────────
|
|
|
|
export function summarizeDraft(d: SasquatchDraft, t: Translate): { title: string; lines: string[]; cta: string } {
|
|
if (d.type === 'post_status') {
|
|
const mins = d.expiresInMinutes ?? 120;
|
|
return {
|
|
title: t('sasquatchUi.draftPostStatusTitle'),
|
|
lines: [`"${d.text}"`, t('sasquatchUi.draftAutoExpires', { count: mins })],
|
|
cta: t('sasquatchUi.draftPostStatusCta'),
|
|
};
|
|
}
|
|
if (d.type === 'wtb_gear') {
|
|
const lines: string[] = [];
|
|
if (d.category) lines.push(t('sasquatchUi.draftCategory', { value: d.category }));
|
|
if (d.size) lines.push(t('sasquatchUi.draftSize', { value: d.size }));
|
|
if (d.condition) lines.push(t('sasquatchUi.draftCondition', { value: d.condition }));
|
|
lines.push(d.price ? t('sasquatchUi.draftMaxPrice', { price: d.price.toLocaleString(), currency: d.currency ?? 'sats' }) : t('sasquatchUi.draftPriceOpen'));
|
|
if (d.description) lines.push(d.description);
|
|
return { title: t('sasquatchUi.draftLookingForTitle', { title: d.title }), lines, cta: t('sasquatchUi.draftWantedCta') };
|
|
}
|
|
const lines: string[] = [];
|
|
if (d.category) lines.push(t('sasquatchUi.draftCategory', { value: d.category }));
|
|
if (d.condition) lines.push(t('sasquatchUi.draftCondition', { value: d.condition }));
|
|
if (d.size) lines.push(t('sasquatchUi.draftSize', { value: d.size }));
|
|
lines.push(d.price ? t('sasquatchUi.draftPrice', { price: d.price.toLocaleString(), currency: d.currency ?? 'sats' }) : t('sasquatchUi.draftPriceOffer'));
|
|
if (d.description) lines.push(d.description);
|
|
return { title: t('sasquatchUi.draftListTitle', { title: d.title }), lines, cta: t('sasquatchUi.draftPublishCta') };
|
|
}
|
|
|
|
// ─── The hook ─────────────────────────────────────────────────────────────────
|
|
|
|
export function useSasquatchActions() {
|
|
const { user } = useCurrentUser();
|
|
const { mutateAsync: publishEvent } = useNostrPublish();
|
|
const { t } = useTranslation();
|
|
|
|
const publishDraft = useCallback(
|
|
async (draft: SasquatchDraft): Promise<{ ok: true; label: string }> => {
|
|
if (!user) throw new Error(t('sasquatchUi.mustBeLoggedIn'));
|
|
|
|
if (draft.type === 'post_status') {
|
|
const tags: string[][] = [['d', 'general']];
|
|
const mins = draft.expiresInMinutes ?? 120;
|
|
tags.push(['expiration', String(Math.floor(Date.now() / 1000) + mins * 60)]);
|
|
await publishEvent({ kind: USER_STATUS_KIND, content: draft.text.slice(0, 140), tags });
|
|
return { ok: true, label: t('sasquatchUi.statusPosted') };
|
|
}
|
|
|
|
if (draft.type === 'wtb_gear') {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const identifier = `wtb-${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
const currency = draft.currency ?? 'sats';
|
|
const tags: string[][] = [
|
|
['d', identifier],
|
|
['title', draft.title.slice(0, 120)],
|
|
['t', 'skateboarding'],
|
|
['listing_type', 'wtb'],
|
|
['status', 'active'],
|
|
['published_at', String(now)],
|
|
['alt', `Wanted: ${draft.title}`],
|
|
];
|
|
if (draft.category) tags.push(['t', draft.category]);
|
|
if (typeof draft.price === 'number' && draft.price > 0) {
|
|
tags.push(['price_type', 'fixed']);
|
|
tags.push(['price', String(draft.price), currency]);
|
|
} else {
|
|
tags.push(['price_type', 'offer']);
|
|
}
|
|
if (draft.condition) tags.push(['condition', draft.condition]);
|
|
if (draft.size) tags.push(['size', draft.size]);
|
|
await publishEvent({
|
|
kind: MARKETPLACE_ITEM_KIND,
|
|
content: (draft.description ?? '').slice(0, 2000),
|
|
tags,
|
|
});
|
|
return { ok: true, label: t('sasquatchUi.wantedPublished') };
|
|
}
|
|
|
|
// list_gear
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const identifier = `gear-${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
const currency = draft.currency ?? 'sats';
|
|
const tags: string[][] = [
|
|
['d', identifier],
|
|
['title', draft.title.slice(0, 120)],
|
|
['t', 'skateboarding'],
|
|
['listing_type', 'sell'],
|
|
['status', 'active'],
|
|
['published_at', String(now)],
|
|
['alt', `Gear listing: ${draft.title}`],
|
|
];
|
|
if (draft.category) tags.push(['t', draft.category]);
|
|
if (typeof draft.price === 'number' && draft.price > 0) {
|
|
tags.push(['price_type', 'fixed']);
|
|
tags.push(['price', String(draft.price), currency]);
|
|
} else {
|
|
tags.push(['price_type', 'offer']);
|
|
}
|
|
if (draft.condition) tags.push(['condition', draft.condition]);
|
|
if (draft.size) tags.push(['size', draft.size]);
|
|
|
|
await publishEvent({
|
|
kind: MARKETPLACE_ITEM_KIND,
|
|
content: (draft.description ?? '').slice(0, 2000),
|
|
tags,
|
|
});
|
|
return { ok: true, label: t('sasquatchUi.listingPublished') };
|
|
},
|
|
[user, publishEvent, t],
|
|
);
|
|
|
|
return { publishDraft };
|
|
}
|