From 45f99a71a1e7b06f0033fcb76411cbee336b8d0a Mon Sep 17 00:00:00 2001 From: samuel1-ona Date: Wed, 8 Oct 2025 20:21:56 +0100 Subject: [PATCH] add username feature --- contracts/tic-tac-toe.clar | 72 ++++++++++++++++++++- frontend/components/navbar.tsx | 68 +++++++++++++++++--- frontend/hooks/use-stacks.ts | 50 ++++++++++++--- frontend/lib/contract.ts | 37 ++++++++++- tests/tic-tac-toe.test.ts | 111 +++++++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+), 17 deletions(-) diff --git a/contracts/tic-tac-toe.clar b/contracts/tic-tac-toe.clar index 4bc01e3..92078ed 100644 --- a/contracts/tic-tac-toe.clar +++ b/contracts/tic-tac-toe.clar @@ -4,6 +4,8 @@ (define-constant ERR_GAME_NOT_FOUND u102) ;; Error thrown when a game cannot be found given a Game ID, i.e. invalid Game ID (define-constant ERR_GAME_CANNOT_BE_JOINED u103) ;; Error thrown when a game cannot be joined, usually because it already has two players (define-constant ERR_NOT_YOUR_TURN u104) ;; Error thrown when a player tries to make a move when it is not their turn +(define-constant ERR-USER-ALREADY-REGISTERED (err u113)) ;; user already registered +(define-constant ERR-USERNAME-EXISTS (err u112)) ;; username already taken ;; The Game ID to use for the next game (define-data-var latest-game-id uint u0) @@ -22,6 +24,73 @@ } ) +;; User registration: map principal to username and registration data +(define-map users + { user: principal } ;; user principal + { + username: (string-utf8 50), ;; username (max 50 chars) + registered-at: uint, ;; block height when registered + } +) + +;; Username to principal mapping (for uniqueness check) +(define-map usernames + { username: (string-utf8 50) } ;; username + { user: principal } ;; user principal +) + +;; Get user registration data by principal +(define-read-only (get-user (user principal)) + (map-get? users { user: user }) +) + +;; ============================================================================= +;; PUBLIC FUNCTIONS - USER REGISTRATION +;; ============================================================================= + +;; register-user(username) +;; Purpose: Register a user with a unique username. +;; Params: +;; - username: (string-utf8 50) desired username (max 50 characters). +;; Preconditions: +;; - User (tx-sender) is not already registered. +;; - Username is not already taken by another user. +;; Effects: +;; - Creates user record in `users` map with username and registration timestamp. +;; - Creates reverse mapping in `usernames` map for uniqueness enforcement. +;; Events: Emits "user-registered" with user principal and username. +;; Returns: (ok true) on success, or appropriate error code. +(define-public (register-user (username (string-utf8 50))) + (let ( + (existing-user (map-get? users { user: tx-sender })) + (existing-username (map-get? usernames { username: username })) + ) + (begin + ;; Validations + (asserts! (is-none existing-user) ERR-USER-ALREADY-REGISTERED) + (asserts! (is-none existing-username) ERR-USERNAME-EXISTS) + + ;; Register user + (map-set users { user: tx-sender } { + username: username, + registered-at: stacks-block-height, + }) + + ;; Track username for uniqueness + (map-set usernames { username: username } { user: tx-sender }) + ;; Emit event + (print { + event: "user-registered", + user: tx-sender, + username: username, + }) + + (ok true) + ) + ) +) + + (define-public (create-game (bet-amount uint) (move-index uint) (move uint)) (let ( ;; Get the Game ID to use for creation of this new game @@ -196,4 +265,5 @@ ;; a-val must equal b-val and must also equal c-val while not being empty (non-zero) (and (is-eq a-val b-val) (is-eq a-val c-val) (not (is-eq a-val u0))) -)) \ No newline at end of file +)) + diff --git a/frontend/components/navbar.tsx b/frontend/components/navbar.tsx index c0ab649..3bd0da2 100644 --- a/frontend/components/navbar.tsx +++ b/frontend/components/navbar.tsx @@ -3,9 +3,27 @@ import { useStacks } from "@/hooks/use-stacks"; import { abbreviateAddress } from "@/lib/stx-utils"; import Link from "next/link"; +import { useState, useRef, useEffect } from "react"; export function Navbar() { - const { userData, connectWallet, disconnectWallet } = useStacks(); + const { userData, connectWallet, disconnectWallet, handleRegisterUser, usernameOnContract } = useStacks(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [username, setUsername] = useState(""); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); return (