diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..67c320a --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run *)", + "Bash(npm test)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(npm install)", + "Bash(git push)" + ] + }, + "$version": 3 +} diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 0000000..1204668 --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run *)" + ] + } +} \ No newline at end of file diff --git a/app/profile/[userId]/page.tsx b/app/profile/[userId]/page.tsx index bb9b6bc..f8b7aa8 100644 --- a/app/profile/[userId]/page.tsx +++ b/app/profile/[userId]/page.tsx @@ -13,6 +13,7 @@ import { EarningsSummary, type EarningsSummary as EarningsSummaryType, } from "@/components/reputation/earnings-summary"; +import { ReputationDashboard } from "@/components/profile/reputation-dashboard"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -21,6 +22,7 @@ import Link from "next/link"; import { useParams } from "next/navigation"; import { useMemo } from "react"; import { useCompletionHistory } from "@/hooks/use-reputation"; +import { ContributorReputationWithMetrics } from "@/types/reputation"; export default function ProfilePage() { const params = useParams(); @@ -204,6 +206,12 @@ export default function ProfilePage() { > Bounty History + + Reputation Dashboard + + +

Reputation Dashboard

+ {"metrics" in reputation && reputation.metrics ? ( + + ) : ( +
+

Reputation metrics are not available yet.

+

+ This feature will be enabled when the backend supports + extended metrics. +

+
+ )} +
+
Detailed analytics coming soon. diff --git a/components/profile/contribution-heatmap.tsx b/components/profile/contribution-heatmap.tsx new file mode 100644 index 0000000..9d199bc --- /dev/null +++ b/components/profile/contribution-heatmap.tsx @@ -0,0 +1,242 @@ +"use client"; + +import { ContributionHistory } from "@/types/reputation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { format, subDays, startOfDay } from "date-fns"; + +interface ContributionHeatmapProps { + contributionHistory: ContributionHistory; + className?: string; +} + +interface DayData { + date: string; + count: number; +} + +// Color levels for different contribution intensities +const getContributionColor = (count: number, maxCount: number): string => { + if (count === 0) return "bg-muted"; + + const ratio = count / (maxCount || 1); + + if (ratio >= 0.8) return "bg-primary"; + if (ratio >= 0.6) return "bg-primary/80"; + if (ratio >= 0.4) return "bg-primary/60"; + if (ratio >= 0.2) return "bg-primary/40"; + return "bg-primary/20"; +}; + +const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +export function ContributionHeatmap({ + contributionHistory, + className, +}: ContributionHeatmapProps) { + const { contributions, totalContributions, streak } = contributionHistory; + + // Generate last 365 days (52 weeks + 1 day = 365 days) + const today = startOfDay(new Date()); + const daysData: DayData[] = []; + + // Build a Map for O(1) lookup instead of O(n) .find() in each iteration + const contributionMap = new Map(contributions.map((c) => [c.date, c.count])); + + for (let i = 364; i >= 0; i--) { + const date = subDays(today, i); + const dateStr = format(date, "yyyy-MM-dd"); + const count = contributionMap.get(dateStr) ?? 0; + daysData.push({ + date: dateStr, + count, + }); + } + + // Organize into weeks for grid display + const weeks: DayData[][] = []; + let currentWeek: DayData[] = []; + + // Find the starting day of week (Sunday) + const startDate = subDays(today, 364); + const startDayOfWeek = startDate.getDay(); + + // Add padding days for the first week + for (let i = 0; i < startDayOfWeek; i++) { + currentWeek.push({ date: "", count: 0 }); + } + + daysData.forEach((day) => { + currentWeek.push(day); + if (currentWeek.length === 7) { + weeks.push(currentWeek); + currentWeek = []; + } + }); + + // Push remaining days + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + currentWeek.push({ date: "", count: 0 }); + } + weeks.push(currentWeek); + } + + const maxCount = Math.max(...daysData.map((d) => d.count), 1); + + // Handle empty data + if (totalContributions === 0) { + return ( + + +
+ + Contribution Activity + +
+
+ +
+

No contributions in the last year

+
+
+
+ ); + } + + return ( + + +
+ + Contribution Activity + +
+ + + {streak.current} + {" "} + day streak + + + + {streak.longest} + {" "} + best + +
+
+
+ + +
+ {/* Month labels */} +
+ {weeks.map((week, weekIndex) => { + // Find the first valid date in this week to determine the month + const firstValidDay = week.find((day) => day.date); + if (!firstValidDay) { + return
; + } + const weekMonth = format(new Date(firstValidDay.date), "MMM"); + const shouldShow = + weekIndex === 0 || + weeks[weekIndex - 1]?.some( + (day) => + day.date && + format(new Date(day.date), "MMM") !== weekMonth, + ); + if (!shouldShow) { + return
; + } + return ( +
+ {weekMonth} +
+ ); + })} +
+ +
+ {/* Weekday labels */} +
+ {WEEKDAYS.map((day, index) => ( +
+ {day} +
+ ))} +
+ + {/* Heatmap grid */} +
+ {weeks.map((week, weekIndex) => ( +
+ {week.map((day, dayIndex) => { + const colorClass = day.date + ? getContributionColor(day.count, maxCount) + : "bg-transparent"; + + return ( + + +
+ + {day.date && ( + +

+ {day.count} contribution + {day.count !== 1 ? "s" : ""} +

+

+ {format(new Date(day.date), "MMM d, yyyy")} +

+
+ )} + + ); + })} +
+ ))} +
+
+ + {/* Legend */} +
+ Less +
+
+
+
+
+
+ More +
+
+ + + + ); +} diff --git a/components/profile/reputation-dashboard.tsx b/components/profile/reputation-dashboard.tsx new file mode 100644 index 0000000..233236e --- /dev/null +++ b/components/profile/reputation-dashboard.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { ContributorReputationWithMetrics } from "@/types/reputation"; +import { XpDisplay } from "./xp-display"; +import { SkillRadarChart } from "./skill-radar-chart"; +import { ContributionHeatmap } from "./contribution-heatmap"; + +interface ReputationDashboardProps { + reputation: ContributorReputationWithMetrics; +} + +export function ReputationDashboard({ reputation }: ReputationDashboardProps) { + const { metrics } = reputation; + + // Guard against missing metrics + if (!metrics) { + return ( +
+

Metrics data is not available for this user.

+
+ ); + } + + return ( +
+ {/* XP Display */} + + + {/* Charts Grid */} +
+ {/* Skill Radar Chart */} + + + {/* Contribution Stats */} +
+ +
+
+
+ ); +} diff --git a/components/profile/skill-radar-chart.tsx b/components/profile/skill-radar-chart.tsx new file mode 100644 index 0000000..0d23046 --- /dev/null +++ b/components/profile/skill-radar-chart.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { SkillLevel } from "@/types/reputation"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Radar, + RadarChart, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + ResponsiveContainer, + Tooltip, +} from "recharts"; +import { cn } from "@/lib/utils"; + +interface SkillRadarChartProps { + skills: SkillLevel[]; + className?: string; +} + +interface ChartData { + skill: string; + level: number; + fullMark: number; +} + +export function SkillRadarChart({ skills, className }: SkillRadarChartProps) { + // Normalize skills to 0-100 scale and prepare chart data + const chartData: ChartData[] = skills.map((skill) => ({ + skill: skill.name, + level: Math.min(Math.max(skill.level, 0), 100), + fullMark: 100, + })); + + // Handle empty skills gracefully + if (skills.length === 0) { + return ( + + + + Technical Skills + + + +
+

No skills data available

+
+
+
+ ); + } + + const maxLevel = Math.max(...skills.map((s) => s.level), 100); + const tickCount = maxLevel >= 100 ? 5 : Math.ceil(maxLevel / 20); + + return ( + + + + Technical Skills + + + +
+ + + + + + + { + if (active && payload && payload.length) { + const data = payload[0].payload as ChartData; + return ( +
+

{data.skill}

+

+ Level: {data.level}/100 +

+
+ ); + } + return null; + }} + /> +
+
+
+
+
+ ); +} diff --git a/components/profile/xp-display.tsx b/components/profile/xp-display.tsx new file mode 100644 index 0000000..5b4f0a7 --- /dev/null +++ b/components/profile/xp-display.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { cn } from "@/lib/utils"; +import { Trophy, Zap } from "lucide-react"; + +interface XpDisplayProps { + xp: number; + className?: string; +} + +// XP thresholds for levels +const getLevelFromXp = (xp: number) => { + // Clamp negative XP to 0 + const clampedXp = Math.max(0, xp); + const level = Math.floor(clampedXp / 1000) + 1; + const currentLevelXp = clampedXp % 1000; + const nextLevelXp = 1000; + const progress = (currentLevelXp / nextLevelXp) * 100; + + return { + level, + currentXp: currentLevelXp, + nextLevelXp, + progress, + }; +}; + +export function XpDisplay({ xp, className }: XpDisplayProps) { + const { level, currentXp, nextLevelXp, progress } = getLevelFromXp(xp); + + return ( + + +
+
+
+ +
+
+

+ Experience Points +

+

+ {xp.toLocaleString()} XP +

+
+
+
+
+ + Level {level} +
+
+
+ +
+
+ Progress to Level {level + 1} + + {currentXp} / {nextLevelXp} XP + +
+ +
+
+
+ ); +} diff --git a/hooks/use-notifications.ts b/hooks/use-notifications.ts index 66b03c1..780aa6e 100644 --- a/hooks/use-notifications.ts +++ b/hooks/use-notifications.ts @@ -91,33 +91,26 @@ export function useNotifications() { const userId = session?.user?.id ?? null; const prevUserIdRef = useRef(userId); - const [notifications, setNotifications] = useState([]); - const [hydrated, setHydrated] = useState(false); - - // Sync state with userId changes during render - // This is faster than useEffect and avoids cascading renders + // Initialize state with lazy loading from localStorage + // This runs only once during initial render, avoiding setState in effect + const [notifications, setNotifications] = useState(() => { + if (typeof window === "undefined" || !userId) return []; + return loadFromStorage(userId); + }); + + // Reset on user change - this is allowed during render as it's a state update + // based on a condition change (userId) if (prevUserIdRef.current !== userId) { prevUserIdRef.current = userId; setNotifications(userId ? loadFromStorage(userId) : []); } - // Handle initial client-side hydration - useEffect(() => { - if (!hydrated) { - if (userId) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setNotifications(loadFromStorage(userId)); - } - setHydrated(true); - } - }, [userId, hydrated]); - // Persist to localStorage whenever notifications change useEffect(() => { - if (userId && hydrated) { + if (userId) { saveToStorage(userId, notifications); } - }, [notifications, userId, hydrated]); + }, [notifications, userId]); // Helper to update notifications with cache invalidation const addNotification = useCallback( @@ -231,7 +224,7 @@ export function useNotifications() { [notifications], ); - const isLoading = session === undefined || !hydrated; + const isLoading = session === undefined; const markAsRead = useCallback((id: string, type: NotificationType) => { setNotifications((previous) => diff --git a/lib/contracts/index.ts b/lib/contracts/index.ts index 4151a14..fcc0569 100644 --- a/lib/contracts/index.ts +++ b/lib/contracts/index.ts @@ -58,7 +58,11 @@ export const projectRegistry = new ProjectRegistryClient({ }); // Re-export transaction helpers for convenience. -export { buildTransaction, simulateContract, submitTransaction } from "./transaction"; +export { + buildTransaction, + simulateContract, + submitTransaction, +} from "./transaction"; // Re-export all client classes and their types. export { BountyRegistryClient } from "./bounty-registry"; diff --git a/lib/contracts/transaction.ts b/lib/contracts/transaction.ts index d45b02b..e6f830a 100644 --- a/lib/contracts/transaction.ts +++ b/lib/contracts/transaction.ts @@ -23,8 +23,7 @@ const RPC_URL = "https://soroban-testnet.stellar.org"; const NETWORK_PASSPHRASE = - process.env.NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE ?? - Networks.TESTNET; + process.env.NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE ?? Networks.TESTNET; const DEFAULT_TIMEOUT_SECONDS = 30; @@ -48,7 +47,10 @@ export async function simulateContract( ): Promise { const server = getServer(); const accountData = await server.getAccount(sourcePublicKey); - const account = new Account(accountData.accountId(), accountData.sequenceNumber()); + const account = new Account( + accountData.accountId(), + accountData.sequenceNumber(), + ); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { @@ -96,7 +98,10 @@ export async function buildTransaction( ): Promise { const server = getServer(); const accountData = await server.getAccount(sourcePublicKey); - const account = new Account(accountData.accountId(), accountData.sequenceNumber()); + const account = new Account( + accountData.accountId(), + accountData.sequenceNumber(), + ); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { @@ -131,7 +136,9 @@ export async function submitTransaction(signedXdr: string): Promise { if (sent.status === "ERROR") { const resultXdr = sent.errorResult?.toXDR("base64") ?? "unknown"; - throw new Error(`Transaction rejected by network. Result XDR: ${resultXdr}`); + throw new Error( + `Transaction rejected by network. Result XDR: ${resultXdr}`, + ); } return sent.hash; diff --git a/package-lock.json b/package-lock.json index c14f2d7..c68c2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,7 +145,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", @@ -159,7 +159,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/@babel/code-frame": { @@ -1101,21 +1101,6 @@ } } }, - "node_modules/@creit-tech/stellar-wallets-kit/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@creit.tech/xbull-wallet-connect": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@creit.tech/xbull-wallet-connect/-/xbull-wallet-connect-0.4.0.tgz", @@ -1157,7 +1142,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1177,7 +1162,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1201,7 +1186,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1229,7 +1214,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1252,7 +1237,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1278,7 +1263,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1300,7 +1284,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6862,7 +6845,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6879,7 +6861,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6896,7 +6877,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6913,7 +6893,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6930,7 +6909,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6947,7 +6925,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6964,7 +6941,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6981,7 +6957,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6998,7 +6973,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7015,7 +6989,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7032,7 +7005,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7049,7 +7021,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7066,7 +7037,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7080,7 +7050,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7102,7 +7071,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7119,7 +7087,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10904,7 +10871,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12184,21 +12150,6 @@ "ws": "^7.5.1" } }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -14840,7 +14791,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^3.2.0", @@ -14998,7 +14949,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", @@ -15134,7 +15085,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -15598,7 +15549,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -16839,7 +16790,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -17634,7 +17584,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" @@ -17698,7 +17648,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -17712,7 +17662,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -18393,7 +18343,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-property": { @@ -18864,21 +18814,6 @@ "ws": "*" } }, - "node_modules/jayson/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/jayson/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -19834,7 +19769,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -19878,7 +19813,7 @@ "version": "26.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", @@ -20141,7 +20076,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20162,7 +20096,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20183,7 +20116,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20204,7 +20136,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20225,7 +20156,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20246,7 +20176,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20267,7 +20196,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20288,7 +20216,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20309,7 +20236,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20330,7 +20256,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20351,7 +20276,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -22368,7 +22292,7 @@ "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -22767,7 +22691,7 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -23287,7 +23211,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -24434,7 +24358,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/run-parallel": { @@ -24558,14 +24482,14 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -25650,7 +25574,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sync-fetch": { @@ -25882,7 +25806,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" @@ -25895,7 +25819,7 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tmpl": { @@ -25952,7 +25876,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -25965,7 +25889,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -27228,7 +27152,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -27261,7 +27185,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -27272,7 +27196,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -27285,7 +27209,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -27298,7 +27222,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -27308,7 +27232,7 @@ "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^5.1.0", @@ -27629,7 +27553,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -27639,7 +27563,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/xrpl": { @@ -27694,7 +27618,7 @@ "version": "2.8.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/types/reputation.ts b/types/reputation.ts index a5f9c56..fd0d690 100644 --- a/types/reputation.ts +++ b/types/reputation.ts @@ -1,88 +1,117 @@ export type ReputationTier = - | 'NEWCOMER' - | 'CONTRIBUTOR' - | 'ESTABLISHED' - | 'EXPERT' - | 'LEGEND'; + | "NEWCOMER" + | "CONTRIBUTOR" + | "ESTABLISHED" + | "EXPERT" + | "LEGEND"; export interface ReputationBreakdown { - features: number; - bugs: number; - documentation: number; - refactoring: number; - other: number; + features: number; + bugs: number; + documentation: number; + refactoring: number; + other: number; } export interface BountyStats { - totalClaimed: number; - totalCompleted: number; - totalEarnings: number; - earningsCurrency: string; - averageCompletionTime: number; // hours - completionRate: number; // percentage 0-100 - currentStreak: number; - longestStreak: number; + totalClaimed: number; + totalCompleted: number; + totalEarnings: number; + earningsCurrency: string; + averageCompletionTime: number; // hours + completionRate: number; // percentage 0-100 + currentStreak: number; + longestStreak: number; } export interface BountyCompletionRecord { - id: string; - bountyId: string; - bountyTitle: string; - projectName: string; - projectLogoUrl: string | null; - difficulty: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED'; - rewardAmount: number; - rewardCurrency: string; - claimedAt: string; - completedAt: string; - completionTimeHours: number; - maintainerRating: number | null; // 1-5 or null - maintainerFeedback: string | null; - pointsEarned: number; + id: string; + bountyId: string; + bountyTitle: string; + projectName: string; + projectLogoUrl: string | null; + difficulty: "BEGINNER" | "INTERMEDIATE" | "ADVANCED"; + rewardAmount: number; + rewardCurrency: string; + claimedAt: string; + completedAt: string; + completionTimeHours: number; + maintainerRating: number | null; // 1-5 or null + maintainerFeedback: string | null; + pointsEarned: number; } export interface ContributorReputation { - id: string; - userId: string; - walletAddress: string | null; - displayName: string; - avatarUrl: string | null; - - // Reputation Metrics - totalScore: number; - tier: ReputationTier; - tierProgress: number; // 0-100 progress to next tier - breakdown: ReputationBreakdown; - - // Activity Stats - stats: BountyStats; - - // Expertise - topTags: string[]; - specializations: string[]; - - // Timestamps - firstBountyAt: string | null; - lastActiveAt: string | null; - createdAt: string; - updatedAt: string; + id: string; + userId: string; + walletAddress: string | null; + displayName: string; + avatarUrl: string | null; + + // Reputation Metrics + totalScore: number; + tier: ReputationTier; + tierProgress: number; // 0-100 progress to next tier + breakdown: ReputationBreakdown; + + // Activity Stats + stats: BountyStats; + + // Expertise + topTags: string[]; + specializations: string[]; + + // Timestamps + firstBountyAt: string | null; + lastActiveAt: string | null; + createdAt: string; + updatedAt: string; } export interface RateContributorInput { - bountyId: string; - contributorId: string; - rating: number; // 1-5 - feedback?: string; + bountyId: string; + contributorId: string; + rating: number; // 1-5 + feedback?: string; } export interface ReputationHistoryParams { - userId: string; - limit?: number; - offset?: number; + userId: string; + limit?: number; + offset?: number; } export interface ReputationHistoryResponse { - records: BountyCompletionRecord[]; - totalCount: number; - hasMore: boolean; + records: BountyCompletionRecord[]; + totalCount: number; + hasMore: boolean; +} + +export interface SkillLevel { + name: string; + level: number; // 0-100 scale +} + +export interface ContributionDay { + date: string; // ISO date string (YYYY-MM-DD) + count: number; // number of contributions +} + +export interface ContributionHistory { + totalContributions: number; + contributions: ContributionDay[]; + streak: { + current: number; + longest: number; + }; +} + +export interface UserReputationMetrics { + xp: number; + skills: SkillLevel[]; + contributionHistory: ContributionHistory; +} + +export interface ContributorReputationWithMetrics extends ContributorReputation { + metrics: UserReputationMetrics; }