Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ coverage
*.info
costs-reports.json
node_modules

# Personal notes and documentation templates
MY_3_POSTS.md
SOCIAL_POSTS_TEMPLATES.md
DEPLOYMENT_STEPS.md
DEPLOYMENT_INFO.md
FEATURE_DOCUMENTATION.md
PR_DESCRIPTION.md
deployments/voting-only.yaml
deployments/deploy-voting-only.yaml
9 changes: 8 additions & 1 deletion Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ authors = []
telemetry = false
cache_dir = './.cache'
requirements = []

[contracts.tic-tac-toe]
path = 'contracts/tic-tac-toe.clar'
clarity_version = 3
epoch = 3.0

[contracts.voting]
path = 'contracts/voting.clar'
clarity_version = 3
epoch = 3.0

[repl.analysis]
passes = ['check_checker']

[repl.analysis.check_checker]
strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false
callee_filter = false
120 changes: 120 additions & 0 deletions contracts/voting.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
;; Voting Contract for Game Proposals
;; Allows community to vote on game rule changes

;; Constants
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-found (err u101))
(define-constant err-already-voted (err u102))
(define-constant err-proposal-closed (err u103))

;; Data Variables
(define-data-var proposal-count uint u0)

;; Data Maps
(define-map proposals
{ proposal-id: uint }
{
title: (string-ascii 100),
description: (string-ascii 500),
proposer: principal,
yes-votes: uint,
no-votes: uint,
is-active: bool,
created-at: uint
}
)

(define-map votes
{ proposal-id: uint, voter: principal }
{ vote: bool }
)

;; Public Functions

;; Create a new proposal
(define-public (create-proposal (title (string-ascii 100)) (description (string-ascii 500)))
(let
(
(proposal-id (var-get proposal-count))
)
(map-set proposals
{ proposal-id: proposal-id }
{
title: title,
description: description,
proposer: tx-sender,
yes-votes: u0,
no-votes: u0,
is-active: true,
created-at: stacks-block-height
}
)
(var-set proposal-count (+ proposal-id u1))
(ok proposal-id)
)
)

;; Vote on a proposal
(define-public (vote (proposal-id uint) (vote-yes bool))
(let
(
(proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) err-not-found))
(voter tx-sender)
)
;; Check if proposal is active
(asserts! (get is-active proposal) err-proposal-closed)

;; Check if user hasn't voted yet
(asserts! (is-none (map-get? votes { proposal-id: proposal-id, voter: voter })) err-already-voted)

;; Record vote
(map-set votes
{ proposal-id: proposal-id, voter: voter }
{ vote: vote-yes }
)

;; Update vote counts
(map-set proposals
{ proposal-id: proposal-id }
(merge proposal {
yes-votes: (if vote-yes (+ (get yes-votes proposal) u1) (get yes-votes proposal)),
no-votes: (if vote-yes (get no-votes proposal) (+ (get no-votes proposal) u1))
})
)
(ok true)
)
)

;; Close a proposal (owner only)
(define-public (close-proposal (proposal-id uint))
(let
(
(proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) err-not-found))
)
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(map-set proposals
{ proposal-id: proposal-id }
(merge proposal { is-active: false })
)
(ok true)
)
)

;; Read-only Functions

(define-read-only (get-proposal (proposal-id uint))
(map-get? proposals { proposal-id: proposal-id })
)

(define-read-only (get-vote (proposal-id uint) (voter principal))
(map-get? votes { proposal-id: proposal-id, voter: voter })
)

(define-read-only (get-proposal-count)
(ok (var-get proposal-count))
)

(define-read-only (has-voted (proposal-id uint) (voter principal))
(is-some (map-get? votes { proposal-id: proposal-id, voter: voter }))
)
25 changes: 25 additions & 0 deletions deployments/default.devnet-plan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
id: 0
name: Devnet deployment
network: devnet
stacks-node: "http://localhost:20443"
bitcoin-node: "http://devnet:devnet@localhost:18443"
plan:
batches:
- id: 0
transactions:
- contract-publish:
contract-name: tic-tac-toe
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 90730
path: "contracts\\tic-tac-toe.clar"
anchor-block-only: true
clarity-version: 3
- contract-publish:
contract-name: voting
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 30230
path: "contracts\\voting.clar"
anchor-block-only: true
clarity-version: 3
epoch: "3.0"
15 changes: 15 additions & 0 deletions deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,43 @@ genesis:
- name: deployer
address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: faucet
address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_1
address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_2
address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_3
address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_4
address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_5
address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_6
address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_7
address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_8
address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP
balance: "100000000000000"
sbtc-balance: "1000000000"
contracts:
- costs
- pox
Expand All @@ -54,4 +64,9 @@ plan:
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/tic-tac-toe.clar
clarity-version: 3
- emulated-contract-publish:
contract-name: voting
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/voting.clar
clarity-version: 3
epoch: "3.0"
11 changes: 9 additions & 2 deletions deployments/default.testnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ plan:
transactions:
- contract-publish:
contract-name: tic-tac-toe
expected-sender: ST3P49R8XXQWG69S66MZASYPTTGNDKK0WW32RRJDN
expected-sender: STA43VC8660WWNRHHWSXGK2VR4BVHGWN0Z63FXGD
cost: 90730
path: contracts/tic-tac-toe.clar
path: "contracts\\tic-tac-toe.clar"
anchor-block-only: true
clarity-version: 3
- contract-publish:
contract-name: voting
expected-sender: STA43VC8660WWNRHHWSXGK2VR4BVHGWN0Z63FXGD
cost: 30230
path: "contracts\\voting.clar"
anchor-block-only: true
clarity-version: 3
epoch: "3.0"
47 changes: 47 additions & 0 deletions frontend/app/voting/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { getAllProposals, type Proposal } from "@/lib/contract";
import { ProposalsList } from "@/components/proposals-list";
import { CreateProposal } from "@/components/create-proposal";
import { useEffect, useState } from "react";

export default function VotingPage() {
const [proposals, setProposals] = useState<Proposal[]>([]);
const [loading, setLoading] = useState(true);

const loadProposals = async () => {
setLoading(true);
try {
const data = await getAllProposals();
setProposals(data);
} catch (error) {
console.error("Error loading proposals:", error);
} finally {
setLoading(false);
}
};

useEffect(() => {
loadProposals();
}, []);

return (
<section className="flex flex-col items-center py-20">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold">Community Voting</h1>
<span className="text-sm text-gray-500">
Propose and vote on game improvements
</span>
</div>

<div className="w-full max-w-4xl space-y-8">
<CreateProposal onProposalCreated={loadProposals} />
{loading ? (
<div className="text-center text-gray-400">Loading proposals...</div>
) : (
<ProposalsList proposals={proposals} onVoted={loadProposals} />
)}
</div>
</section>
);
}
84 changes: 84 additions & 0 deletions frontend/components/create-proposal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { useStacks } from "@/hooks/use-stacks";
import { useState } from "react";

export function CreateProposal() {
const { userData, handleCreateProposal } = useStacks();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title || !description) return;

setIsSubmitting(true);
try {
await handleCreateProposal(title, description);
setTitle("");
setDescription("");
} finally {
setIsSubmitting(false);
}
};

if (!userData) {
return (
<div className="bg-gray-800 rounded-lg p-6 text-center">
<p className="text-gray-400">Connect your wallet to create proposals</p>
</div>
);
}

return (
<div className="bg-gray-800 rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Create New Proposal</h2>
<form onSubmit={onSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium mb-2">
Title
</label>
<input
id="title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
maxLength={100}
placeholder="Add 5x5 board option"
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isSubmitting}
required
/>
<p className="text-xs text-gray-500 mt-1">{title.length}/100 characters</p>
</div>

<div>
<label htmlFor="description" className="block text-sm font-medium mb-2">
Description
</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={500}
placeholder="Allow players to choose a larger board size for more complex games"
rows={4}
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isSubmitting}
required
/>
<p className="text-xs text-gray-500 mt-1">{description.length}/500 characters</p>
</div>

<button
type="submit"
disabled={isSubmitting || !title || !description}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-600 disabled:cursor-not-allowed px-6 py-3 rounded-lg font-medium transition-colors"
>
{isSubmitting ? "Creating..." : "Create Proposal"}
</button>
</form>
</div>
);
}
Loading