The marketplace Reputation tab showed a bare 'Loading...' text under 'Reviews & trade records', and TradeRecommendationsCard rendered its 'No recommendations yet.' card while still loading (then it vanished for a self/empty view) — together they read as broken. - Reviews/trade-records now render row-shaped skeletons while loading. - TradeRecommendationsCard renders nothing while its query is loading when the count is 0 (and only shows for a non-self viewer who can add one).
117 lines
4.4 KiB
TypeScript
117 lines
4.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import {
|
|
useTraderRecommendations,
|
|
useMyRecommendation,
|
|
useRecommendationActions,
|
|
} from '@/hooks/useTraderRecommendations';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import { ShieldCheck, Loader2 } from 'lucide-react';
|
|
import { useTranslation } from '@/contexts/language';
|
|
|
|
function Recommender({ pubkey }: { pubkey: string }) {
|
|
const author = useAuthor(pubkey);
|
|
const name = author.data?.metadata?.name ?? genUserName(pubkey);
|
|
return (
|
|
<Link to={`/${nip19.npubEncode(pubkey)}`} className="flex items-center gap-2">
|
|
<Avatar className="h-6 w-6 border border-white/10">
|
|
<AvatarImage src={author.data?.metadata?.picture} alt={name} />
|
|
<AvatarFallback className="bg-gray-800 text-[10px] text-gray-400">
|
|
{name.slice(0, 2).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="truncate text-xs text-gray-300 hover:text-white">{name}</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Trusted-trader recommendations for a profile (kind 31873). Shows who vouches
|
|
* for this trader, and lets a logged-in viewer add their own recommendation.
|
|
*/
|
|
export function TradeRecommendationsCard({ pubkey, className }: { pubkey: string; className?: string }) {
|
|
const { t } = useTranslation();
|
|
const { user } = useCurrentUser();
|
|
const { data: recs, isLoading } = useTraderRecommendations(pubkey);
|
|
const { data: mine } = useMyRecommendation(pubkey);
|
|
const { recommend } = useRecommendationActions();
|
|
const { toast } = useToast();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const isSelf = user?.pubkey === pubkey;
|
|
const count = recs?.length ?? 0;
|
|
|
|
// Don't flash an empty "No recommendations yet." card while the query is still
|
|
// loading (it then vanishes for a self/empty view) — render nothing until we
|
|
// know, and only show the card when there's something (or the viewer can add).
|
|
if (count === 0 && (isLoading || !user || isSelf)) return null;
|
|
|
|
const handleRecommend = async () => {
|
|
setBusy(true);
|
|
try {
|
|
await recommend(pubkey);
|
|
toast({ title: t('marketUi.recommendations.published'), description: t('marketUi.recommendations.publishedBody') });
|
|
} catch (e) {
|
|
toast({
|
|
title: t('marketUi.recommendations.failed'),
|
|
description: e instanceof Error ? e.message : t('marketUi.recommendations.somethingWrong'),
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className={`bg-black/90 border-2 border-[#ff6600]/25 ${className ?? ''}`}>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-lg text-[#ff6600]">
|
|
<ShieldCheck className="h-5 w-5" />
|
|
{t('marketUi.recommendations.title')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<p className="text-sm text-gray-300">
|
|
{count > 0 ? (
|
|
<>
|
|
{t('marketUi.recommendations.recommendedBy')} <span className="font-bold text-white">{count}</span> {count === 1 ? t('marketUi.recommendations.skaterOne') : t('marketUi.recommendations.skaterMany')}.
|
|
</>
|
|
) : (
|
|
t('marketUi.recommendations.none')
|
|
)}
|
|
</p>
|
|
|
|
{count > 0 && (
|
|
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
|
{recs!.slice(0, 8).map((r) => (
|
|
<Recommender key={r.id} pubkey={r.recommender} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{user && !isSelf && (
|
|
mine ? (
|
|
<p className="text-xs text-[#ccff00]">{t('marketUi.recommendations.youRecommend')}</p>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
onClick={handleRecommend}
|
|
disabled={busy}
|
|
className="w-full gap-2 bg-[#ff6600] font-bold text-black hover:bg-[#ff6600]/90"
|
|
>
|
|
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <ShieldCheck className="h-4 w-4" />}
|
|
{t('marketUi.recommendations.recommend')}
|
|
</Button>
|
|
)
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|