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
+
+
+
+
+
+
+
+
+ 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;
}