From d03f710ec3bf2c9ca17e2140697936fd7e2188c3 Mon Sep 17 00:00:00 2001 From: code mayor Date: Thu, 30 Jul 2026 07:19:12 +0100 Subject: [PATCH] feat: implement reputation system for contributors - Add ContributorReputation type, DataKey, storage, and functions to smart contract - Add ContributorReputationUpdated event for on-chain reputation tracking - Expose get_contributor_reputation and increase_contributor_reputation in contract interface - Add frontend reputation types (ContributorReputation, Badge) - Add reputation store (getContributorReputation, increaseContributorReputation) to task-workflow - Automatically increase reputation score when submissions are approved - Add GET /api/reputation?contributor=
API endpoint - Display reputation score and completed tasks count in ContributorProfileCard - System is extensible for future badges via Badge type --- .../hello-world/src/autoshare_logic.rs | 61 +++++++++++++++++- .../contracts/hello-world/src/base/events.rs | 11 ++++ .../contracts/hello-world/src/base/types.rs | 11 ++++ contract/contracts/hello-world/src/lib.rs | 25 ++++++++ .../components/ContributorProfileCard.tsx | 62 ++++++++++++++++--- frontend/src/app/api/reputation/route.ts | 42 +++++++++++++ frontend/src/lib/task-workflow.ts | 52 ++++++++++++++++ frontend/src/types/reputation.ts | 37 +++++++++++ 8 files changed, 291 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/api/reputation/route.ts create mode 100644 frontend/src/types/reputation.ts diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index 5858fbc..ac221fd 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -1,9 +1,9 @@ use crate::base::errors::Error; use crate::base::events::{ AdminTransferred, AutoshareCreated, AutoshareUpdated, ContractPaused, ContractUnpaused, - GroupActivated, GroupDeactivated, Withdrawal, + ContributorReputationUpdated, GroupActivated, GroupDeactivated, Withdrawal, }; -use crate::base::types::{AutoShareDetails, GroupMember, PaymentHistory}; +use crate::base::types::{AutoShareDetails, ContributorReputation, GroupMember, PaymentHistory}; use crate::base::validation::{ validate_fee, validate_name, validate_usage_count, validate_withdraw_amount, }; @@ -22,6 +22,8 @@ pub enum DataKey { GroupPaymentHistory(BytesN<32>), GroupMembers(BytesN<32>), IsPaused, + /// Stores ContributorReputation for each contributor address. + Reputation(Address), } pub fn create_autoshare( @@ -810,3 +812,58 @@ fn validate_members(members: &Vec) -> Result<(), Error> { } Ok(()) } + +// ============================================================================ +// Reputation System +// ============================================================================ + +/// Returns the current reputation for a contributor. +/// Returns a default (score=0, completed_tasks=0) if no reputation exists yet. +pub fn get_contributor_reputation(env: Env, contributor: Address) -> ContributorReputation { + let key = DataKey::Reputation(contributor.clone()); + env.storage() + .persistent() + .get(&key) + .unwrap_or(ContributorReputation { + contributor, + score: 0, + completed_tasks: 0, + }) +} + +/// Increases a contributor's reputation score by the given amount. +/// Increments completed_tasks by 1. +/// Emits a ContributorReputationUpdated event. +/// Designed to be extensible: future badge logic can be added here. +pub fn increase_contributor_reputation( + env: Env, + contributor: Address, + score_increase: u32, +) -> Result<(), Error> { + contributor.require_auth(); + + let key = DataKey::Reputation(contributor.clone()); + let mut reputation: ContributorReputation = env + .storage() + .persistent() + .get(&key) + .unwrap_or(ContributorReputation { + contributor: contributor.clone(), + score: 0, + completed_tasks: 0, + }); + + reputation.score += score_increase; + reputation.completed_tasks += 1; + + env.storage().persistent().set(&key, &reputation); + + ContributorReputationUpdated { + contributor: contributor.clone(), + new_score: reputation.score, + completed_tasks: reputation.completed_tasks, + } + .publish(&env); + + Ok(()) +} diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index e8cd9db..4ffc1c2 100644 --- a/contract/contracts/hello-world/src/base/events.rs +++ b/contract/contracts/hello-world/src/base/events.rs @@ -57,3 +57,14 @@ pub struct Withdrawal { pub recipient: Address, pub amount: i128, } + +/// Emitted when a contributor's reputation score changes. +/// The new score reflects the updated reputation after a submission is accepted. +#[contractevent(data_format = "single-value")] +#[derive(Clone)] +pub struct ContributorReputationUpdated { + #[topic] + pub contributor: Address, + pub new_score: u32, + pub completed_tasks: u32, +} diff --git a/contract/contracts/hello-world/src/base/types.rs b/contract/contracts/hello-world/src/base/types.rs index a2d1d04..237cae6 100644 --- a/contract/contracts/hello-world/src/base/types.rs +++ b/contract/contracts/hello-world/src/base/types.rs @@ -28,3 +28,14 @@ pub struct PaymentHistory { pub amount_paid: i128, pub timestamp: u64, } + +/// Represents a contributor's reputation score. +/// The score increases when a submission is accepted. +/// Designed to be extensible for future badge logic. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContributorReputation { + pub contributor: Address, + pub score: u32, + pub completed_tasks: u32, +} diff --git a/contract/contracts/hello-world/src/lib.rs b/contract/contracts/hello-world/src/lib.rs index 25681fe..c717a85 100644 --- a/contract/contracts/hello-world/src/lib.rs +++ b/contract/contracts/hello-world/src/lib.rs @@ -235,6 +235,31 @@ impl AutoShareContract { pub fn reduce_usage(env: Env, id: BytesN<32>, caller: Address) { autoshare_logic::reduce_usage(env, id, caller).unwrap(); } + + // ============================================================================ + // Reputation System + // ============================================================================ + + /// Returns the current reputation for a contributor. + /// Returns a default (score=0, completed_tasks=0) if no reputation exists yet. + pub fn get_contributor_reputation( + env: Env, + contributor: Address, + ) -> base::types::ContributorReputation { + autoshare_logic::get_contributor_reputation(env, contributor) + } + + /// Increases a contributor's reputation score by the given amount. + /// Increments completed_tasks by 1. + /// Emits a ContributorReputationUpdated event. + /// Designed to be extensible: future badge logic can be added here. + pub fn increase_contributor_reputation( + env: Env, + contributor: Address, + score_increase: u32, + ) { + autoshare_logic::increase_contributor_reputation(env, contributor, score_increase).unwrap(); + } } // 3. Link the tests (Requirement: Unit Tests) diff --git a/frontend/src/app/(dashboard)/user/overview/components/ContributorProfileCard.tsx b/frontend/src/app/(dashboard)/user/overview/components/ContributorProfileCard.tsx index 67d4f73..2c1918c 100644 --- a/frontend/src/app/(dashboard)/user/overview/components/ContributorProfileCard.tsx +++ b/frontend/src/app/(dashboard)/user/overview/components/ContributorProfileCard.tsx @@ -1,13 +1,14 @@ "use client"; import { motion } from "framer-motion"; -import { CheckCircle2, Circle, UserRound } from "lucide-react"; +import { Award, CheckCircle2, Circle, Star, UserRound } from "lucide-react"; import React from "react"; import { calculateContributorProfileCompletion, type ContributorProfileField, } from "@/lib/contributor-profile"; +import { getContributorReputation } from "@/lib/task-workflow"; const PROFILE_FIELDS: ContributorProfileField[] = [ { key: "name", label: "Full name" }, @@ -19,7 +20,9 @@ const PROFILE_FIELDS: ContributorProfileField[] = [ ]; export default function ContributorProfileCard() { - const [profile, setProfile] = React.useState>({ + const [profile, setProfile] = React.useState< + Record + >({ name: "Alicia Chen", headline: "Product designer", bio: "", @@ -33,6 +36,11 @@ export default function ContributorProfileCard() { [profile], ); + const reputation = React.useMemo( + () => getContributorReputation("current-user"), + [], + ); + const updateField = (key: string, value: string) => { setProfile((current) => ({ ...current, [key]: value })); }; @@ -50,14 +58,18 @@ export default function ContributorProfileCard() {
-

Contributor profile

+

+ Contributor profile +

Finish the profile to make your work easier to discover.

-
{completion.percentage}%
+
+ {completion.percentage}% +
complete
@@ -73,6 +85,32 @@ export default function ContributorProfileCard() {
+ {/* Reputation Section */} +
+
+
+ + + Reputation + +
+
+ {reputation.score} +
+
+
+
+ + + Completed + +
+
+ {reputation.completedTasks} +
+
+
+
{PROFILE_FIELDS.map((field) => { const isComplete = Boolean(profile[field.key]?.trim()); @@ -95,7 +133,9 @@ export default function ContributorProfileCard() { )}
- + {isComplete ? "Filled" : "Missing"} @@ -103,7 +143,9 @@ export default function ContributorProfileCard() { {isTextArea ? (