From 301b17e95d204423dcd0fb3a4229de049268d87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Carri=C3=B3n?= Date: Fri, 24 Apr 2026 19:47:17 -0300 Subject: [PATCH] feat: create badge component --- components/reputation-badge.tsx | 209 ++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 components/reputation-badge.tsx diff --git a/components/reputation-badge.tsx b/components/reputation-badge.tsx new file mode 100644 index 0000000..34a2915 --- /dev/null +++ b/components/reputation-badge.tsx @@ -0,0 +1,209 @@ +"use client"; +import { useState } from "react"; +import { ShieldCheck, Star, Scale, CheckCircle2, Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export type BadgeType = + | "verified-freelancer" + | "top-rated" + | "dispute-free" + | "100-completion"; + +export type BadgeSize = "sm" | "md" | "lg"; + +export interface ReputationBadgeProps { + type: BadgeType; + txHash?: string; + explorerUrl?: string; + verify?: (txHash?: string) => Promise; + size?: BadgeSize; + showLabel?: boolean; + className?: string; +} + +type BadgeMeta = { + label: string; + description: string; + Icon: typeof ShieldCheck; + containerClass: string; + iconClass: string; + ringClass: string; +}; + +const BADGES: Record = { + "verified-freelancer": { + label: "Verified Freelancer", + description: "Identity verified on-chain via attestation.", + Icon: ShieldCheck, + containerClass: + "bg-[var(--badge-verified-bg)] text-[var(--badge-verified-fg)] border-[var(--badge-verified-border)]", + iconClass: "text-[var(--badge-verified-fg)]", + ringClass: "ring-[var(--badge-verified-fg)]/30", + }, + "top-rated": { + label: "Top Rated", + description: "Consistently rated 4.8★ or higher by clients.", + Icon: Star, + containerClass: + "bg-[var(--badge-top-bg)] text-[var(--badge-top-fg)] border-[var(--badge-top-border)]", + iconClass: "text-[var(--badge-top-fg)]", + ringClass: "ring-[var(--badge-top-fg)]/30", + }, + "dispute-free": { + label: "Dispute-Free", + description: "No client disputes recorded on-chain.", + Icon: Scale, + containerClass: + "bg-[var(--badge-dispute-bg)] text-[var(--badge-dispute-fg)] border-[var(--badge-dispute-border)]", + iconClass: "text-[var(--badge-dispute-fg)]", + ringClass: "ring-[var(--badge-dispute-fg)]/30", + }, + "100-completion": { + label: "100% Completion", + description: "Every contract delivered and accepted on-chain.", + Icon: CheckCircle2, + containerClass: + "bg-[var(--badge-completion-bg)] text-[var(--badge-completion-fg)] border-[var(--badge-completion-border)]", + iconClass: "text-[var(--badge-completion-fg)]", + ringClass: "ring-[var(--badge-completion-fg)]/30", + }, +}; + +const SIZES: Record< + BadgeSize, + { wrap: string; icon: string; text: string; gap: string } +> = { + sm: { + wrap: "px-2 py-1 rounded-md", + icon: "h-3.5 w-3.5", + text: "text-xs", + gap: "gap-1", + }, + md: { + wrap: "px-3 py-1.5 rounded-lg", + icon: "h-4 w-4", + text: "text-sm", + gap: "gap-1.5", + }, + lg: { + wrap: "px-4 py-2 rounded-xl", + icon: "h-5 w-5", + text: "text-base", + gap: "gap-2", + }, +}; + +const explorer = "https://etherscan.io/tx/"; +const demoHash = + "0x9f2c1a7b4e6d3f8a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a"; + +const verifyTest = async (hash?: string) => { + await new Promise((r) => setTimeout(r, 700)); + return Boolean(hash); +}; + +export function ReputationBadge({ + type, + txHash = demoHash, + explorerUrl = explorer, + verify = verifyTest, + size = "md", + showLabel = true, + className, +}: ReputationBadgeProps) { + const meta = BADGES[type]; + const sizing = SIZES[size]; + const { Icon } = meta; + + const [status, setStatus] = useState< + "idle" | "verifying" | "verified" | "failed" + >(txHash ? "idle" : "idle"); + + const handleVerify = async () => { + if (status === "verifying") return; + setStatus("verifying"); + try { + const ok = verify ? await verify(txHash) : Boolean(txHash); + setStatus(ok ? "verified" : "failed"); + } catch { + setStatus("failed"); + } + }; + + return ( + + ); +} + +export interface ReputationBadgeGroupProps { + badges: Array & { id?: string }>; + size?: BadgeSize; + className?: string; +} + +export function ReputationBadgeGroup({ + badges, + size = "md", + className, +}: ReputationBadgeGroupProps) { + return ( +
+ {badges.map((b, i) => ( +
+ +
+ ))} +
+ ); +} + +export default ReputationBadge;