fix(marketplace): show an outage instead of an empty trade/reputation state
TradesPage, SellerDashboard, TradeReputationPanel, TradeReputationCard and ShippingStatusCard now surface a relay outage explicitly instead of rendering nothing-waiting / no-trades / hidden reputation. Shared marketUi.loadFailed (en/es).
This commit is contained in:
parent
7a5ec82f84
commit
7a60099bab
8 changed files with 38 additions and 11 deletions
|
|
@ -222,6 +222,11 @@ Second batch (payment, attestation, escrow):
|
||||||
escrow (`item.escrow === 'required'`); ask-seller is available on an active
|
escrow (`item.escrow === 'required'`); ask-seller is available on an active
|
||||||
listing (not only once sold); payment receipts are scoped to the payer's
|
listing (not only once sold); payment receipts are scoped to the payer's
|
||||||
pubkey, so a stale/foreign receipt cannot be attached by another account.
|
pubkey, so a stale/foreign receipt cannot be attached by another account.
|
||||||
|
14. **Outage states (trade/reputation/seller/shipping):** `TradesPage`,
|
||||||
|
`SellerDashboard`, `TradeReputationPanel`, `TradeReputationCard` and
|
||||||
|
`ShippingStatusCard` now show an explicit "could not reach the relays"
|
||||||
|
notice (`marketUi.loadFailed`) instead of an empty / "nothing waiting" /
|
||||||
|
"no trades" state when their query errors.
|
||||||
|
|
||||||
## 7. Recommended order
|
## 7. Recommended order
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ function MyListingRow({ item }: { item: MarketplaceItem }) {
|
||||||
export function SellerDashboard() {
|
export function SellerDashboard() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { data: items, isLoading } = useMarketplaceItems(200);
|
const { data: items, isLoading, isError } = useMarketplaceItems(200);
|
||||||
|
|
||||||
const mine = (items ?? []).filter((i) => i.pubkey === user?.pubkey);
|
const mine = (items ?? []).filter((i) => i.pubkey === user?.pubkey);
|
||||||
const active = mine.filter((i) => i.status !== 'sold');
|
const active = mine.filter((i) => i.status !== 'sold');
|
||||||
|
|
@ -98,7 +98,9 @@ export function SellerDashboard() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isError ? (
|
||||||
|
<p className="text-sm text-amber-400">{t('marketUi.loadFailed')}</p>
|
||||||
|
) : isLoading ? (
|
||||||
<p className="text-sm text-gray-500">{t('marketUi.seller.loading')}</p>
|
<p className="text-sm text-gray-500">{t('marketUi.seller.loading')}</p>
|
||||||
) : mine.length === 0 ? (
|
) : mine.length === 0 ? (
|
||||||
<Card className="border-dashed border-white/10">
|
<Card className="border-dashed border-white/10">
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function ShippingStatusCard({ item }: { item: MarketplaceItem }) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const listingRef = { pubkey: item.pubkey, identifier: item.identifier };
|
const listingRef = { pubkey: item.pubkey, identifier: item.identifier };
|
||||||
const { data: trade } = useListingTrade(listingRef, user?.pubkey);
|
const { data: trade } = useListingTrade(listingRef, user?.pubkey);
|
||||||
const { data: statuses = [] } = useShipmentStatus(listingRef);
|
const { data: statuses = [], isError: statusError } = useShipmentStatus(listingRef);
|
||||||
const { setStatus, isPending } = useSetShipmentStatus();
|
const { setStatus, isPending } = useSetShipmentStatus();
|
||||||
const [tracking, setTracking] = useState('');
|
const [tracking, setTracking] = useState('');
|
||||||
|
|
||||||
|
|
@ -80,6 +80,8 @@ export function ShippingStatusCard({ item }: { item: MarketplaceItem }) {
|
||||||
{issue && <span className="text-amber-400">· {t('marketUi.shipping.issue')}</span>}
|
{issue && <span className="text-amber-400">· {t('marketUi.shipping.issue')}</span>}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{statusError && <p className="text-xs text-amber-400">{t('marketUi.loadFailed')}</p>}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={tracking}
|
value={tracking}
|
||||||
onChange={(e) => setTracking(e.target.value)}
|
onChange={(e) => setTracking(e.target.value)}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function Stat({ label, value, accent }: { label: string; value: number; accent?:
|
||||||
*/
|
*/
|
||||||
export function TradeReputationCard({ pubkey, className }: { pubkey: string; className?: string }) {
|
export function TradeReputationCard({ pubkey, className }: { pubkey: string; className?: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useTradeReputation(pubkey);
|
const { data, isError } = useTradeReputation(pubkey);
|
||||||
const verifiedWith = data?.verifiedWith ?? [];
|
const verifiedWith = data?.verifiedWith ?? [];
|
||||||
const partnerPubkeys = verifiedWith.map((v) => v.pubkey);
|
const partnerPubkeys = verifiedWith.map((v) => v.pubkey);
|
||||||
// Trust-weight each mutually-confirmed trade by the counterparty's web-of-trust
|
// Trust-weight each mutually-confirmed trade by the counterparty's web-of-trust
|
||||||
|
|
@ -36,6 +36,10 @@ export function TradeReputationCard({ pubkey, className }: { pubkey: string; cla
|
||||||
const { data: rings } = useTradeRings(pubkey, partnerPubkeys);
|
const { data: rings } = useTradeRings(pubkey, partnerPubkeys);
|
||||||
|
|
||||||
const total = (data?.sales ?? 0) + (data?.purchases ?? 0);
|
const total = (data?.sales ?? 0) + (data?.purchases ?? 0);
|
||||||
|
// A relay outage must not silently read as "no reputation".
|
||||||
|
if (isError) {
|
||||||
|
return <p className="text-sm text-amber-400">{t('marketUi.loadFailed')}</p>;
|
||||||
|
}
|
||||||
if (!data || total === 0) return null;
|
if (!data || total === 0) return null;
|
||||||
|
|
||||||
const people = data.verifiedCounterparties;
|
const people = data.verifiedCounterparties;
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ function ReviewSkeleton() {
|
||||||
*/
|
*/
|
||||||
export function TradeReputationPanel({ pubkey }: { pubkey: string }) {
|
export function TradeReputationPanel({ pubkey }: { pubkey: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data, isLoading } = useTradeReputation(pubkey);
|
const { data, isLoading, isError } = useTradeReputation(pubkey);
|
||||||
const attestations = [...(data?.attestations ?? [])].sort((a, b) => b.created_at - a.created_at);
|
const attestations = [...(data?.attestations ?? [])].sort((a, b) => b.created_at - a.created_at);
|
||||||
const reviews = attestations.filter((a) => a.pubkey !== pubkey);
|
const reviews = attestations.filter((a) => a.pubkey !== pubkey);
|
||||||
const ratings = attestations
|
const ratings = attestations
|
||||||
|
|
@ -126,7 +126,9 @@ export function TradeReputationPanel({ pubkey }: { pubkey: string }) {
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isLoading ? (
|
{isError ? (
|
||||||
|
<p className="text-sm text-amber-400">{t('marketUi.loadFailed')}</p>
|
||||||
|
) : isLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<ReviewSkeleton />
|
<ReviewSkeleton />
|
||||||
<ReviewSkeleton />
|
<ReviewSkeleton />
|
||||||
|
|
|
||||||
|
|
@ -1772,6 +1772,7 @@ marketUi: {
|
||||||
paidWithProof: 'Saved a verifiable receipt — confirm the trade to make it count.',
|
paidWithProof: 'Saved a verifiable receipt — confirm the trade to make it count.',
|
||||||
paidBody: 'Ask the seller to confirm the trade so it counts for both of you.',
|
paidBody: 'Ask the seller to confirm the trade so it counts for both of you.',
|
||||||
},
|
},
|
||||||
|
loadFailed: 'Could not reach the relays. Check your connection and try again.',
|
||||||
offers: {
|
offers: {
|
||||||
accepted: 'Accepted',
|
accepted: 'Accepted',
|
||||||
declined: 'Declined',
|
declined: 'Declined',
|
||||||
|
|
|
||||||
|
|
@ -1772,6 +1772,7 @@ marketUi: {
|
||||||
paidWithProof: 'Se guardó un recibo verificable: confirma el trato para que cuente.',
|
paidWithProof: 'Se guardó un recibo verificable: confirma el trato para que cuente.',
|
||||||
paidBody: 'Pide al vendedor que confirme el trato para que cuente para ambos.',
|
paidBody: 'Pide al vendedor que confirme el trato para que cuente para ambos.',
|
||||||
},
|
},
|
||||||
|
loadFailed: 'No se pudo contactar con los relays. Comprueba tu conexión e inténtalo de nuevo.',
|
||||||
offers: {
|
offers: {
|
||||||
accepted: 'Aceptada',
|
accepted: 'Aceptada',
|
||||||
declined: 'Rechazada',
|
declined: 'Rechazada',
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,11 @@ export default function TradesPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { publishTrade, isPending: publishing } = useTradeAttestation();
|
const { publishTrade, isPending: publishing } = useTradeAttestation();
|
||||||
|
|
||||||
const { data: incoming, isLoading: loadingIncoming } = useIncomingTradeRequests(user?.pubkey);
|
const { data: incoming, isLoading: loadingIncoming, isError: incomingError } = useIncomingTradeRequests(user?.pubkey);
|
||||||
const { data: pending, isLoading: loadingPending } = usePendingTradeConfirmations(user?.pubkey);
|
const { data: pending, isLoading: loadingPending, isError: pendingError } = usePendingTradeConfirmations(user?.pubkey);
|
||||||
const { data: reputation, isLoading: loadingRep } = useTradeReputation(user?.pubkey);
|
const { data: reputation, isLoading: loadingRep, isError: repError } = useTradeReputation(user?.pubkey);
|
||||||
|
// A relay outage must not look like "nothing waiting" / "no trades".
|
||||||
|
const tradeError = incomingError || pendingError || repError;
|
||||||
|
|
||||||
const [busy, setBusy] = useState<string | null>(null);
|
const [busy, setBusy] = useState<string | null>(null);
|
||||||
const [revokeTarget, setRevokeTarget] = useState<{
|
const [revokeTarget, setRevokeTarget] = useState<{
|
||||||
|
|
@ -226,7 +228,11 @@ export default function TradesPage() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!loadingIncoming && !loadingPending && actionNeeded === 0 && (
|
{tradeError && (
|
||||||
|
<p className="text-sm text-amber-400">{t('marketUi.loadFailed')}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!tradeError && !loadingIncoming && !loadingPending && actionNeeded === 0 && (
|
||||||
<p className="text-sm text-gray-500">{t('marketUi.trades.nothingWaiting')}</p>
|
<p className="text-sm text-gray-500">{t('marketUi.trades.nothingWaiting')}</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -239,7 +245,11 @@ export default function TradesPage() {
|
||||||
|
|
||||||
{loadingRep && <Skeleton className="h-16 w-full rounded-2xl" />}
|
{loadingRep && <Skeleton className="h-16 w-full rounded-2xl" />}
|
||||||
|
|
||||||
{!loadingRep && authored.length === 0 && (
|
{repError && (
|
||||||
|
<p className="text-sm text-amber-400">{t('marketUi.loadFailed')}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!repError && !loadingRep && authored.length === 0 && (
|
||||||
<Card className="border-dashed border-white/15 bg-black/60">
|
<Card className="border-dashed border-white/15 bg-black/60">
|
||||||
<CardContent className="py-10 text-center">
|
<CardContent className="py-10 text-center">
|
||||||
<Package className="mx-auto mb-3 h-8 w-8 text-gray-600" />
|
<Package className="mx-auto mb-3 h-8 w-8 text-gray-600" />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue