- First-visit language auto-detect (Spanish devices land in es; stored choice wins). - Sasquatch AI replies in the app language (skate trick names kept English). - i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t() key missing from en/es or any en/es key drift. - ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace, trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters. - Policy: only skate TRICK names stay English; user-generated content untouched.
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { useState, type ComponentType, type ReactNode } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { DeckRating } from '@/components/DeckRating';
|
||
import { cn } from '@/lib/utils';
|
||
import { useTranslation } from '@/contexts/language';
|
||
|
||
interface TradeConfirmRowProps {
|
||
tone: 'sale' | 'purchase';
|
||
icon: ComponentType<{ className?: string }>;
|
||
text: ReactNode;
|
||
listingUrl: string;
|
||
busy: boolean;
|
||
disabled: boolean;
|
||
actionLabel: string;
|
||
/** Called with the chosen deck rating (0 = none). */
|
||
onConfirm: (rating: number) => void;
|
||
}
|
||
|
||
/**
|
||
* A pending trade to confirm, with a 1–10 skateboard-deck rating the reviewer
|
||
* can set before confirming. The rating rides along on the kind-31871
|
||
* attestation, so it becomes the review.
|
||
*/
|
||
export function TradeConfirmRow({
|
||
tone,
|
||
icon: Icon,
|
||
text,
|
||
listingUrl,
|
||
busy,
|
||
disabled,
|
||
actionLabel,
|
||
onConfirm,
|
||
}: TradeConfirmRowProps) {
|
||
const { t } = useTranslation();
|
||
const [rating, setRating] = useState(0);
|
||
|
||
const cardCls = tone === 'sale' ? 'border-[#ff6600]/30 bg-[#ff6600]/5' : 'border-[#ccff00]/30 bg-[#ccff00]/5';
|
||
const iconCls = tone === 'sale' ? 'text-[#ff6600]' : 'text-[#ccff00]';
|
||
const btnCls = tone === 'sale' ? 'bg-[#ff6600] hover:bg-[#ff6600]/90' : 'bg-[#ccff00] hover:bg-[#ccff00]/90';
|
||
|
||
return (
|
||
<Card className={cn('border-2', cardCls)}>
|
||
<CardContent className="flex flex-col gap-2 p-3">
|
||
<div className="flex items-center gap-3">
|
||
<Icon className={cn('h-5 w-5 shrink-0', iconCls)} />
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-sm text-white">{text}</p>
|
||
<Link to={listingUrl} className="text-xs text-gray-400 hover:text-white">
|
||
{t('marketUi.trades.viewListing')} <ArrowRight className="inline h-3 w-3" />
|
||
</Link>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
onClick={() => onConfirm(rating)}
|
||
disabled={disabled}
|
||
className={cn('shrink-0 gap-1.5 font-bold text-black', btnCls)}
|
||
>
|
||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <CheckCircle className="h-3.5 w-3.5" />}
|
||
{actionLabel}
|
||
</Button>
|
||
</div>
|
||
<div className="flex items-center gap-2 pl-8">
|
||
<span className="shrink-0 text-[11px] text-gray-500">{t('marketUi.confirmRow.rateTrade')}</span>
|
||
<DeckRating value={rating} onChange={setRating} size="sm" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|