Initial setup

This commit is contained in:
Raja Ilayaperumal
2022-09-06 10:25:15 +05:30
parent 64fef5adce
commit aec8167171
17755 changed files with 1820188 additions and 1 deletions

View File

@@ -1 +1,34 @@
# ballot
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

191
clarity/ballot.clar Normal file
View File

@@ -0,0 +1,191 @@
;; ballot
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-constant CONTRACT-OWNER tx-sender)
;; Errors
(define-constant ERR-NOT-OWNER (err u1403))
(define-constant ERR-NOT-STARTED (err u1001))
(define-constant ERR-ENDED (err u1002))
(define-constant ERR-ALREADY-VOTED (err u1003))
(define-constant ERR-INVALID-VOTING-SYSTEM (err u1004))
(define-constant ERR-NOT-HOLDING-BNS (err u1005))
(define-constant ERR-FAILED-STRATEGY (err u1006))
(define-constant ERR-NOT-VOTED (err u1007))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; data maps and vars
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-data-var title (string-utf8 512) u"")
(define-data-var description (string-utf8 512) u"")
(define-data-var voting-system (string-ascii 10) "")
(define-data-var start uint u0)
(define-data-var end uint u0)
(define-data-var should-be-a-bns-holder bool false)
(define-map results {id: (string-ascii 36)} {count: uint, name: (string-utf8 512)} )
(define-map users {id: principal} {count: uint, vote: (list 2 (string-ascii 36)), volume: (list 2 uint)})
(define-map register {id: uint} {user: principal, bns: (string-ascii 256), vote: (list 2 (string-ascii 36)), volume: (list 2 uint)})
(define-data-var total uint u0)
(define-data-var options (list 2 (string-ascii 36)) (list))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; private functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; (define-private (am-i-bns-holder (domain (buff 20)) (namespace (buff 48)))
;; (let
;; (
;; (dns-owner (get owner (unwrap-panic (contract-call? 'SP000000000000000000002Q6VF78.bns name-resolve domain namespace))))
;; )
;; (if (is-eq tx-sender dns-owner)
;; true
;; false
;; )
;; )
;; )
(define-private (have-i-voted)
(match (map-get? users {id: tx-sender})
success true
false
)
)
(define-private (voting-system-validation (length uint))
(if (is-eq (var-get voting-system) "single")
(if (is-eq length u1)
true
false
)
true
)
)
;; (define-private (validate-strategy (token-id uint))
;; (let
;; (
;; (nft-owner-optional (unwrap-panic (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract get-owner token-id)))
;; )
;; (match nft-owner-optional
;; nft-owner
;; (if (is-eq tx-sender nft-owner)
;; true
;; false
;; )
;; false
;; )
;; )
;; )
(define-private (fold-boolean (left bool) (right bool))
(and (is-eq left true) (is-eq right true))
)
(define-private (check-volume (each-volume uint))
(> each-volume u0)
)
(define-private (validate-vote-volume (volume (list 2 uint)))
(begin
(fold fold-boolean (map check-volume volume) true)
)
)
(define-private (process-my-vote (option-id (string-ascii 36)) (volume uint))
(match (map-get? results {id: option-id})
result (let
(
(new-count-tuple {count: (+ volume (get count result))})
)
;; Capture the vote
(map-set results {id: option-id} (merge result new-count-tuple))
;; Return
true
)
(begin
(map-set results {id: option-id} {count: u1, name: u""})
;; Return
true
)
)
)
(define-private (get-single-result (option-id (string-ascii 36)))
(let
(
(volume (default-to u0 (get count (map-get? results {id: option-id}))))
)
;; Return volume
volume
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; public functions for all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-public (cast-my-vote (vote (list 2 (string-ascii 36))) (volume (list 2 uint))
(bns (string-ascii 256)) (domain (buff 20)) (namespace (buff 48)) (token-id uint)
)
(let
(
(next-total (+ u1 (var-get total)))
)
;; Validation
(asserts! (and (> (len vote) u0) (is-eq (len vote) (len volume)) (validate-vote-volume volume)) ERR-NOT-VOTED)
(asserts! (voting-system-validation (len vote)) ERR-INVALID-VOTING-SYSTEM)
(asserts! (>= block-height (var-get start)) ERR-NOT-STARTED)
(asserts! (<= block-height (var-get end)) ERR-ENDED)
(asserts! (not (have-i-voted)) ERR-ALREADY-VOTED)
;; (asserts! (validate-strategy token-id) ERR-FAILED-STRATEGY)
;; (asserts! (or (not (var-get should-be-a-bns-holder)) (am-i-bns-holder domain namespace)) ERR-NOT-HOLDING-BNS)
;; Register the vote
(map process-my-vote vote volume)
(map-set users {id: tx-sender} {count: u1, vote: vote, volume: volume})
(map-set register {id: next-total} {user: tx-sender, bns: bns, vote: vote, volume: volume})
;; Increase the total
(var-set total next-total)
;; Return
(ok true)
)
)
(define-read-only (get-results)
(begin
(ok {total: (var-get total),options: (var-get options), results: (map get-single-result (var-get options))})
)
)
(define-read-only (get-result-at-position (position uint))
(ok (map-get? register {id: position}))
)
(define-read-only (get-result-by-user (user principal))
(ok (map-get? users {id: user}))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; public functions for contract owner
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Default assignments
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(var-set title u"BlockSurvey Poll")
(var-set description u"Description")
(var-set voting-system "single")
(var-set options (list "option1" "option2"))
(var-set start u0)
(var-set end u0)
(map-set results {id: "option1"} {count: u0, name: u"Yes"})
(map-set results {id: "option2"} {count: u0, name: u"No"})
;; (var-set should-be-a-bns-holder true)

40
clarity/nft.clar Normal file
View File

@@ -0,0 +1,40 @@
;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)
(define-constant contract-owner tx-sender)
(define-constant err-owner-only (err u100))
(define-constant err-not-token-owner (err u101))
(define-non-fungible-token stacksies uint)
(define-data-var last-token-id uint u0)
(define-read-only (get-last-token-id)
(ok (var-get last-token-id))
)
(define-read-only (get-token-uri (token-id uint))
(ok none)
)
(define-read-only (get-owner (token-id uint))
(ok (nft-get-owner? stacksies token-id))
)
(define-public (transfer (token-id uint) (sender principal) (recipient principal))
(begin
(asserts! (is-eq tx-sender sender) err-not-token-owner)
(nft-transfer? stacksies token-id sender recipient)
)
)
(define-public (mint (recipient principal))
(let
(
(token-id (+ (var-get last-token-id) u1))
)
(asserts! (is-eq tx-sender contract-owner) err-owner-only)
(try! (nft-mint? stacksies token-id recipient))
(var-set last-token-id token-id)
(ok token-id)
)
)

8
common/constants.js Normal file
View File

@@ -0,0 +1,8 @@
export const Constants = {
// Stacks mainnet network flag
STACKS_MAINNET_FLAG:
process.env.NEXT_PUBLIC_STACKS_MAINNET_FLAG === "false" ? false : true,
// IPFS gateway
IPFS_GATEWAY: "https://ipfs.owl.link/ipfs/"
};

334
common/contract.js Normal file
View File

@@ -0,0 +1,334 @@
import { openContractCall, openContractDeploy } from "@stacks/connect";
import { AnchorMode, bufferCV, listCV, stringAsciiCV, uintCV } from "@stacks/transactions";
import { getNetworkType } from "../services/auth";
export async function deployContract(pollObject, contractName, callbackFunction) {
const contract = getContract(pollObject);
// Transaction options
const txOptions = {
contractName: contractName,
codeBody: contract,
network: getNetworkType(),
anchorMode: AnchorMode.Any,
appDetails: {
name: "Ballot",
icon: window.location.origin + "/images/logo/ballot.png"
},
onFinish: callbackFunction,
};
// Call contract function
await openContractDeploy(txOptions);
}
function getContract(pollObject) {
let contract = getRawContract();
let optionIds;
let optionResults;
pollObject?.options?.forEach(option => {
if (!optionIds) {
optionIds = `"${option?.id}"`
optionResults = `(map-set results {id: "${option?.id}"} {count: u0, name: u"${getStringByLength(option?.value, 512)}"})`
} else {
optionIds = optionIds + ` "${option?.id}"`;
optionResults = optionResults + ` (map-set results {id: "${option?.id}"} {count: u0, name: u"${getStringByLength(option?.value, 512)}"})`
}
});
// Strategy
let strategyContractFunction = "";
let validateStrategy = "";
if (pollObject?.strategyContractName) {
strategyContractFunction = getStrategyContractFunction(pollObject?.strategyContractName);
validateStrategy = `(asserts! (validate-strategy token-id) ERR-FAILED-STRATEGY)`;
}
// .btc dns holder validation
let btcDnsHolderContractFunction = "";
let validateBtcDnsHolder = "";
if (pollObject?.votingStrategyTemplate == "btcholders") {
strategyContractFunction = getBtcDnsHolderContractFunction();
validateStrategy = `(asserts! (am-i-btc-dns-holder domain namespace) ERR-NOT-HOLDING-BTC-DNS)`;
}
const placeholder = {
"noOfOptions": pollObject?.options?.length,
"title": getStringByLength(pollObject?.title, 512),
"description": getStringByLength(pollObject?.description, 512),
"votingSystem": pollObject?.votingSystem,
"startAtBlock": pollObject?.startAtBlock,
"endAtBlock": pollObject?.endAtBlock,
"optionIds": optionIds,
"optionResults": optionResults,
"strategyContractFunction": strategyContractFunction,
"validateStrategy": validateStrategy,
"btcDnsHolderContractFunction": btcDnsHolderContractFunction,
"validateBtcDnsHolder": validateBtcDnsHolder
}
for (let key in placeholder) {
const searchRegExp = new RegExp(`&{${key}}`, 'gi')
const replaceWith = placeholder[key];
contract = contract.replace(searchRegExp, replaceWith);
}
return contract;
}
function getStringByLength(text, length) {
// Make it single line string
let finalText = text.replace(/\n/g, "");
// Cut shot by string length
finalText = finalText.slice(0, length);
return finalText;
}
function getRawContract() {
return `;; ballot
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-constant CONTRACT-OWNER tx-sender)
;; Errors
(define-constant ERR-NOT-OWNER (err u1403))
(define-constant ERR-NOT-STARTED (err u1001))
(define-constant ERR-ENDED (err u1002))
(define-constant ERR-ALREADY-VOTED (err u1003))
(define-constant ERR-INVALID-VOTING-SYSTEM (err u1004))
(define-constant ERR-NOT-HOLDING-BTC-DNS (err u1005))
(define-constant ERR-FAILED-STRATEGY (err u1006))
(define-constant ERR-NOT-VOTED (err u1007))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; data maps and vars
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-data-var title (string-utf8 512) u"")
(define-data-var description (string-utf8 512) u"")
(define-data-var voting-system (string-ascii 10) "")
(define-data-var start uint u0)
(define-data-var end uint u0)
(define-data-var should-be-a-bns-holder bool false)
(define-map results {id: (string-ascii 36)} {count: uint, name: (string-utf8 512)} )
(define-map users {id: principal} {count: uint, vote: (list &{noOfOptions} (string-ascii 36)), volume: (list &{noOfOptions} uint)})
(define-map register {id: uint} {user: principal, bns: (string-ascii 256), vote: (list &{noOfOptions} (string-ascii 36)), volume: (list &{noOfOptions} uint)})
(define-data-var total uint u0)
(define-data-var options (list &{noOfOptions} (string-ascii 36)) (list))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; private functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
&{btcDnsHolderContractFunction}
(define-private (have-i-voted)
(match (map-get? users {id: tx-sender})
success true
false
)
)
(define-private (voting-system-validation (length uint))
(if (is-eq (var-get voting-system) "single")
(if (is-eq length u1)
true
false
)
true
)
)
&{strategyContractFunction}
(define-private (fold-boolean (left bool) (right bool))
(and (is-eq left true) (is-eq right true))
)
(define-private (check-volume (each-volume uint))
(> each-volume u0)
)
(define-private (validate-vote-volume (volume (list &{noOfOptions} uint)))
(begin
(fold fold-boolean (map check-volume volume) true)
)
)
(define-private (process-my-vote (option-id (string-ascii 36)) (volume uint))
(match (map-get? results {id: option-id})
result (let
(
(new-count-tuple {count: (+ volume (get count result))})
)
;; Capture the vote
(map-set results {id: option-id} (merge result new-count-tuple))
;; Return
true
)
(begin
(map-set results {id: option-id} {count: u1, name: u""})
;; Return
true
)
)
)
(define-private (get-single-result (option-id (string-ascii 36)))
(let
(
(volume (default-to u0 (get count (map-get? results {id: option-id}))))
)
;; Return volume
volume
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; public functions for all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-public (cast-my-vote (vote (list &{noOfOptions} (string-ascii 36))) (volume (list &{noOfOptions} uint))
(bns (string-ascii 256)) (domain (buff 20)) (namespace (buff 48)) (token-id uint)
)
(let
(
(next-total (+ u1 (var-get total)))
)
;; Validation
(asserts! (and (> (len vote) u0) (is-eq (len vote) (len volume)) (validate-vote-volume volume)) ERR-NOT-VOTED)
(asserts! (voting-system-validation (len vote)) ERR-INVALID-VOTING-SYSTEM)
(asserts! (>= block-height (var-get start)) ERR-NOT-STARTED)
(asserts! (<= block-height (var-get end)) ERR-ENDED)
(asserts! (not (have-i-voted)) ERR-ALREADY-VOTED)
&{validateStrategy}
&{validateBtcDnsHolder}
;; Register the vote
(map process-my-vote vote volume)
(map-set users {id: tx-sender} {count: u1, vote: vote, volume: volume})
(map-set register {id: next-total} {user: tx-sender, bns: bns, vote: vote, volume: volume})
;; Increase the total
(var-set total next-total)
;; Return
(ok true)
)
)
(define-read-only (get-results)
(begin
(ok {total: (var-get total),options: (var-get options), results: (map get-single-result (var-get options))})
)
)
(define-read-only (get-result-at-position (position uint))
(ok (map-get? register {id: position}))
)
(define-read-only (get-result-by-user (user principal))
(ok (map-get? users {id: user}))
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; public functions for contract owner
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Default assignments
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(var-set title u"&{title}")
(var-set description u"&{description}")
(var-set voting-system "&{votingSystem}")
(var-set options (list &{optionIds}))
(var-set start u&{startAtBlock})
(var-set end u&{endAtBlock})
&{optionResults}`;
}
function getStrategyContractFunction(strategyContractName) {
return `(define-private (validate-strategy (token-id uint))
(let
(
(nft-owner-optional (unwrap-panic (contract-call? '${strategyContractName} get-owner token-id)))
)
(match nft-owner-optional
nft-owner
(if (is-eq tx-sender nft-owner)
true
false
)
false
)
)
)`;
}
function getBtcDnsHolderContractFunction() {
return `(define-private (am-i-btc-dns-holder (domain (buff 20)) (namespace (buff 48)))
(let
(
(dns-owner (get owner (unwrap-panic (contract-call? 'SP000000000000000000002Q6VF78.bns name-resolve domain namespace))))
)
(if (and (is-eq domain 0x627463) (is-eq tx-sender dns-owner))
true
false
)
)
)`;
}
export async function castMyVoteContractCall(contractAddress, contractName, voteObj, dns, tokenId, callbackFunction) {
// Parse vote
let voteStringAsciiArr = [], volumeUIntArr = [];
for (let key in voteObj) {
voteStringAsciiArr.push(stringAsciiCV(key));
volumeUIntArr.push(uintCV(voteObj[key]));
}
// Parse dns
let domain, namespace;
if (dns && dns.split(".").length <= 1) {
// Parse dns
let splittedDns = dns.split(".");
domain = splittedDns.pop();
namespace = splittedDns.join(".");
}
const functionArgs = [
listCV(voteStringAsciiArr),
listCV(volumeUIntArr),
stringAsciiCV(dns ? dns : ""),
bufferCV(Buffer.from(domain ? domain : "")),
bufferCV(Buffer.from(namespace ? namespace : "")),
uintCV(tokenId ? tokenId : 0)
];
// Contract function details to be called
const options = {
contractAddress: contractAddress,
contractName: contractName,
functionName: "cast-my-vote",
functionArgs: functionArgs,
postConditions: [],
network: getNetworkType(),
appDetails: {
name: "Ballot",
icon: window.location.origin + "/images/logo/owllink.png",
},
onFinish: callbackFunction,
};
// Call contract function
await openContractCall(options);
}

25
common/utils.js Normal file
View File

@@ -0,0 +1,25 @@
import { getStacksAPIPrefix } from "../services/auth";
import { Constants } from "./constants";
export async function getRecentBlock() {
// Get btc domain for logged in user
const response = await fetch(
getStacksAPIPrefix() + "/extended/v1/block?limit=1"
);
const responseObject = await response.json();
return responseObject?.results?.[0];
}
export function formStacksExplorerUrl(txId) {
return (
"https://explorer.stacks.co/txid/" +
txId +
"?chain=" +
(Constants.STACKS_MAINNET_FLAG ? "mainnet" : "testnet")
);
}
export function convertToDisplayDateFormat(date) {
return new Date(date).toLocaleDateString('general', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', hour12: true, minute: 'numeric' })
}

View File

@@ -0,0 +1,577 @@
import { useEffect, useState } from "react";
import { Col, Container, Row, Button, Form } from "react-bootstrap";
import styles from "../../styles/Builder.module.css";
import { getFileFromGaia, getMyStxAddress, getUserData, putFileToGaia } from "../../services/auth.js"
import { v4 as uuidv4 } from "uuid";
import { nanoid } from 'nanoid';
import Link from "next/link";
import Router from 'next/router'
import PreviewComponent from "./Preview.component";
import { getRecentBlock } from "../../common/utils";
import { deployContract } from "../../common/contract";
export default function BuilderComponent(props) {
// Variables
// Poll id and Gaia address
const { pathParams } = props;
// Poll id
const [pollId, setPollId] = useState();
// Draft mode or not
const [mode, setMode] = useState();
// Poll object
const [pollObject, setPollObject] = useState();
// Processing flag
const [isProcessing, setIsProcessing] = useState(false);
// Error message
const [errorMessage, setErrorMessage] = useState("");
// Show preview
const [show, setShow] = useState(false);
const handleShow = () => setShow(true);
const handleClose = () => setShow(false);
const [currentProgressMessage, setCurrentProgressMessage] = useState();
// Default strategy templates
const strategyTemplates = {
"satoshibles": {
"strategyNFTName": "Satoshibles",
"strategyContractName": "SP6P4EJF0VG8V0RB3TQQKJBHDQKEF6NVRD1KZE3C.satoshibles"
}, "crashpunks": {
"strategyNFTName": "crashpunks-v2",
"strategyContractName": "SP3QSAJQ4EA8WXEDSRRKMZZ29NH91VZ6C5X88FGZQ.crashpunks-v2"
}, "theexplorerguild": {
"strategyNFTName": "The-Explorer-Guild",
"strategyContractName": "SP2X0TZ59D5SZ8ACQ6YMCHHNR2ZN51Z32E2CJ173.the-explorer-guild"
}, "stacksparrots": {
"strategyNFTName": "stacks-parrots",
"strategyContractName": "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.byzantion-stacks-parrots"
}, "blocksurvey": {
"strategyNFTName": "blocksurvey",
"strategyContractName": "https://explorer.stacks.co/txid/SPNWZ5V2TPWGQGVDR6T7B6RQ4XMGZ4PXTEE0VQ0S.blocksurvey"
}, "btcholders": {
"strategyNFTName": "",
"strategyContractName": ""
}
}
// Functions
useEffect(() => {
if (pathParams && pathParams?.[0]) {
setPollId(pathParams[0]);
}
if (pathParams && pathParams?.[1]) {
setMode(pathParams[1]);
} else {
setMode("");
}
// Fetch from Gaia
if (pollId) {
if (pollId === "new") {
// Initialize new poll
setPollObject(initializeNewPoll());
} else if (mode === "draft") {
getFileFromGaia(pollId + ".json", {}).then(
(response) => {
if (response) {
setPollObject(JSON.parse(response));
}
},
(error) => {
// File does not exit in gaia
if (error && error.code == "does_not_exist") {
// Initialize new poll
setPollObject(initializeNewPoll());
}
});
} else if (pollId) {
// Fetch from IPFS
fetch(`https://owllink.mypinata.cloud/ipfs/${pollId}`)
.then(response => response.json())
.then(data => setPollObject(data));
}
}
}, [pathParams, pollId, mode]);
function initializeNewPoll() {
return {
title: "",
description: "",
votingSystem: "single",
options: [
{
id: uuidv4(),
value: "Choice 1"
},
{
id: uuidv4(),
value: "Choice 2"
}
],
strategyContractName: "",
startAtBlock: 0,
endAtBlock: 0,
startAtDate: 0,
endAtDate: 0,
id: uuidv4(),
status: "draft",
createdAt: new Date(),
username: getUserData().identityAddress,
userStxAddress: getMyStxAddress()
}
}
const handleChange = e => {
const { name, value } = e.target;
// Switch box component
if (name == "votingStrategyFlag") {
value = e.target.checked;
// Reset the values
if (value == false) {
pollObject["votingStrategyTemplate"] = "";
pollObject["strategyNFTName"] = "";
pollObject["strategyContractName"] = "";
}
} else if (name == "votingStrategyTemplate") {
if (strategyTemplates[value]) {
pollObject["strategyNFTName"] = strategyTemplates[value]["strategyNFTName"];
pollObject["strategyContractName"] = strategyTemplates[value]["strategyContractName"];
} else {
pollObject["strategyNFTName"] = "";
pollObject["strategyContractName"] = "";
}
}
// If value is empty, then delete key from previous state
if (!value && pollObject) {
// Delete key from JSON
delete pollObject[name];
} else {
// Update the value
pollObject[name] = value;
}
setPollObject({ ...pollObject });
};
const addOption = () => {
if (pollObject?.options) {
pollObject.options.push({
id: uuidv4(),
value: `Choice ${pollObject.options.length + 1}`
})
}
setPollObject({ ...pollObject });
}
const deleteOption = (index) => {
if (pollObject?.options && index < pollObject?.options?.length) {
pollObject.options.splice(index, 1);
setPollObject({ ...pollObject });
}
}
const handleOptionChange = (e, option) => {
option.value = e.target.value
setPollObject({ ...pollObject });
}
const savePollToGaia = (encrypt = true) => {
if (pollObject?.id) {
// Start processing
setIsProcessing(true);
setCurrentProgressMessage("Saving ...");
// Reset message
setErrorMessage("");
// Save to gaia
putFileToGaia(`${pollObject.id}.json`, JSON.stringify(pollObject), (encrypt ? {} : { "encrypt": false })).then(response => {
// Fetch and Update poll index
fetchAndUpdatePollIndex();
});
}
}
const fetchAndUpdatePollIndex = () => {
getFileFromGaia("pollIndex.json", {}).then(
(response) => {
if (response) {
updatePollIndex(JSON.parse(response));
}
},
(error) => {
// File does not exit in gaia
if (error && error.code == "does_not_exist") {
// Initialize new
const newPollIndexObj = {
list: [],
ref: {}
};
updatePollIndex(newPollIndexObj);
}
});
}
const updatePollIndex = (currentPollIndexObj) => {
if (currentPollIndexObj?.list && currentPollIndexObj?.ref) {
// Insert to list
if (!currentPollIndexObj.ref[pollObject.id]) {
// New index
currentPollIndexObj.list.push(pollObject.id);
}
// Create/Update index
currentPollIndexObj.ref[pollObject.id] = {
"id": pollObject.id,
"title": pollObject.title,
"description": pollObject.description,
"username": pollObject.username,
"createdAt": pollObject.createdAt,
"updatedAt": new Date(),
"status": pollObject.status,
"startAt": pollObject.startAtDate,
"endAt": pollObject.endAtDate,
"publishedInfo": pollObject?.publishedInfo,
"ipfsLocation": pollObject?.ipfsLocation
};
putFileToGaia("pollIndex.json", JSON.stringify(currentPollIndexObj), {}).then(response => {
if (pollObject?.ipfsLocation) {
const gaiaAddress = getUserData()?.gaiaHubConfig?.address;
Router.replace("/p/" + pollObject?.id + "/" + gaiaAddress + "/results");
} else if (pollId === "new") {
Router.replace("/builder/" + pollObject.id + "/draft");
setPollId(pollObject.id);
}
// Stop processing
setIsProcessing(false);
setCurrentProgressMessage("");
});
}
}
const validatePoll = () => {
if (!pollObject) return "Poll is not yet created.";
if (!pollObject?.title || !pollObject?.description ||
!pollObject?.options || pollObject?.options?.length == 0) {
return "Please fill all required fields."
}
if (!pollObject?.startAtDate || !pollObject?.endAtDate) {
return "Please select poll start and end date."
} else if (new Date() > new Date(pollObject?.startAtDate)) {
return "Start date should be a future date."
} else if (new Date() > new Date(pollObject?.endAtDate) ||
new Date(pollObject?.startAtDate) > new Date(pollObject?.endAtDate)) {
return "End date should be greater than start date."
}
if (pollObject?.votingStrategyTemplate == "other" && (!pollObject?.strategyNFTName || !pollObject?.strategyContractName)) {
return "Please enter strategy NFT name or Contract Address"
}
}
const calculateBlockTime = (targetTimestamp, currentBlockHeight) => {
const currentTimestamp = new Date().getTime();
if (targetTimestamp && targetTimestamp > currentTimestamp && currentBlockHeight > 0) {
const diff = Math.abs(new Date(targetTimestamp) - new Date(currentTimestamp));
const minutes = Math.floor((diff / 1000) / 60);
const blockTime = Math.round((1 / 10) * minutes);
return currentBlockHeight + blockTime;
}
return 0;
}
const publishPoll = async () => {
// Start processing
setIsProcessing(true);
// Reset message
setErrorMessage("");
// Validation
const _errorMessage = validatePoll();
if (_errorMessage) {
setErrorMessage(_errorMessage);
// Start processing
setIsProcessing(false);
return;
}
// Show message
setCurrentProgressMessage("Publishing contract ...");
// Get current Block
const currentBlock = await getRecentBlock();
// Calculate block time
if (pollObject?.startAtDate) {
pollObject['startAtBlock'] = calculateBlockTime(new Date(pollObject?.startAtDate).getTime(), currentBlock?.height);
}
if (pollObject?.endAtDate) {
pollObject['endAtBlock'] = calculateBlockTime(new Date(pollObject?.endAtDate).getTime(), currentBlock?.height);
}
const contractName = "ballot-" + getTitleWithOutSpecialChar();
pollObject["publishedInfo"] = {
"contractAddress": getMyStxAddress(),
"contractName": contractName
}
publishContract(contractName);
}
const publishContract = (contractName) => {
// Publish contract
deployContract(pollObject, contractName, callbackFunction);
}
const callbackFunction = (data) => {
if (data?.txId) {
// Update the contract deployed information
pollObject.publishedInfo["txId"] = data?.txId;
// Update the status
pollObject["status"] = "live";
publishPollToIPFS();
}
}
const publishPollToIPFS = async () => {
setCurrentProgressMessage("Publishing to IPFS ...");
// Publish JSON to IPFS
const data = JSON.stringify({
"pinataOptions": {
"cidVersion": 1
},
"pinataMetadata": {
"name": pollObject?.title,
"keyvalues": {
"id": pollObject?.id
}
},
"pinataContent": pollObject
});
fetch('https://api.pinata.cloud/pinning/pinJSONToIPFS', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySW5mb3JtYXRpb24iOnsiaWQiOiI0MDgzMzAwOC0wNWE2LTQxNzYtYjZlNy01ZGZjYzliOTg0NTYiLCJlbWFpbCI6InJhamFAYmxvY2tzdXJ2ZXkub3JnIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBpbl9wb2xpY3kiOnsicmVnaW9ucyI6W3siaWQiOiJGUkExIiwiZGVzaXJlZFJlcGxpY2F0aW9uQ291bnQiOjF9XSwidmVyc2lvbiI6MX0sIm1mYV9lbmFibGVkIjpmYWxzZSwic3RhdHVzIjoiQUNUSVZFIn0sImF1dGhlbnRpY2F0aW9uVHlwZSI6InNjb3BlZEtleSIsInNjb3BlZEtleUtleSI6ImIwOWMzZjVjMTQ1ZWMxMjIwNGIxIiwic2NvcGVkS2V5U2VjcmV0IjoiMGUxMWNhZGZhMDFiMmQ3MGQ4YTJiZWMwZTRmNzBkMWJiN2I2NWZlYTA3OWQzZjNkNGI1YmQ5ODk0M2U4MTk3ZiIsImlhdCI6MTY2MTc4NjY4N30.qmkn_YrtU1jJExIpqZLN3FfMqbIzciuerWIUUutCayc'
},
body: data
}).then(async (response) => {
const responseBody = await response.json();
// Update the IPFS location
pollObject["ipfsLocation"] = responseBody?.IpfsHash
setPollObject({ ...pollObject });
// Save poll to gaia
savePollToGaia(false);
});
}
const getTitleWithOutSpecialChar = () => {
return pollObject?.title?.replace(/[^a-zA-Z0-9]/g, '');
}
return (
<>
<Container fluid>
<Row className="justify-content-md-center">
<Col lg={8} md={12}>
<div className={styles.builder_container}>
{/* Title */}
<h4>{pollId && pollId === "new" ? "New" : "Edit"} Poll</h4>
{pollObject && pollObject.id ?
<Form style={{ marginTop: "20px" }}>
<Form.Group className="mb-3">
<Form.Label>Title</Form.Label>
<Form.Control type="text" name="title" value={pollObject.title} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Description</Form.Label>
<Form.Control as="textarea" name="description" value={pollObject.description} rows={5} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Voting system</Form.Label>
<div>
{['Single', 'Multiple'].map((option, index) => (
<Form.Check
inline
key={index}
type='radio'
name="votingSystem"
value={option.toLowerCase()}
checked={pollObject.votingSystem === option.toLowerCase()}
id={`voting_system_${option}`}
label={option}
onChange={handleChange}
/>
))}
</div>
{/* List of options */}
<div style={{ margin: "10px 0 0 10px" }}>
{pollObject?.options &&
pollObject.options.map((option, index) => (
<div key={index} style={{ margin: "5px 0", display: "flex", alignItems: "center" }}>
{pollObject?.votingSystem === "single" ?
<Form.Check style={{ marginRight: "10px" }}
type='radio'
disabled
/>
:
<Form.Check style={{ marginRight: "10px" }}
type='checkbox'
disabled
/>
}
<Form.Control type="text" placeholder="" value={option?.value} onChange={e => handleOptionChange(e, option)} />
<Button variant="secondary" style={{ marginLeft: "10px", width: "80px" }} onClick={() => { deleteOption(index); }}>Delete</Button>
</div>
))
}
</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button variant="secondary" style={{ width: "75px" }} onClick={() => { addOption(); }}>Add</Button>
</div>
</Form.Group>
{/* Voting Period */}
<div>
<Form.Label>Voting period</Form.Label>
<Form.Group className="mb-3" style={{ display: "flex", alignItems: "center" }}>
<Form.Label style={{ marginRight: "10px", width: "50px" }}>Start</Form.Label>
<Form.Control type="datetime-local" name="startAtDate" value={pollObject.startAtDate} style={{ width: "250px" }}
min={new Date().toISOString().slice(0, 16)}
onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3" style={{ display: "flex", alignItems: "center" }}>
<Form.Label style={{ marginRight: "10px", width: "50px" }}>End</Form.Label>
<Form.Control type="datetime-local" name="endAtDate" value={pollObject.endAtDate} style={{ width: "250px" }}
onChange={handleChange} disabled={!pollObject?.startAtDate} min={pollObject?.startAtDate} />
</Form.Group>
</div>
{/* Voting Strategy */}
<div>
<div style={{ display: "flex", alignItems: "center", padding: "10px 0" }}>
<Form.Label>Voting strategy</Form.Label>
<Form.Check style={{ marginLeft: "10px" }}
inline
type="switch"
id="voting-strategy-id"
name="votingStrategyFlag"
onChange={handleChange}
/>
</div>
{pollObject?.votingStrategyFlag &&
<>
<Form.Group className="mb-3">
<Form.Label>Default strategy</Form.Label>
<Form.Select id="voting-strategy-template" name="votingStrategyTemplate" defaultValue={""} onChange={handleChange}>
<option disabled value="">Select</option>
<option value="satoshibles">Satoshibles</option>
<option value="crashpunks">CrashPunks</option>
<option value="theexplorerguild">The Explorer Guild</option>
<option value="stacksparrots">Stacks Parrots</option>
<option value="blocksurvey">BlockSurvey</option>
<option value="btcholders">.btc holders</option>
<option value="other">Other</option>
</Form.Select>
</Form.Group>
{/* Only for other */}
{pollObject?.votingStrategyTemplate && pollObject?.votingStrategyTemplate == "other" &&
<>
<Form.Group className="mb-3">
<Form.Label>NFT name</Form.Label>
<Form.Control type="text" name="strategyNFTName" value={pollObject.strategyNFTName}
onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Contract name</Form.Label>
<Form.Control type="text" name="strategyContractName" value={pollObject.strategyContractName}
onChange={handleChange}
placeholder="ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract" />
</Form.Group>
</>
}
</>
}
</div>
{/* Error Message */}
{errorMessage &&
<div style={{ margin: "10px 0" }}>
<span style={{ fontSize: "14px" }}>{errorMessage}</span>
</div>
}
{/* CTA */}
{
pollId !== "new" &&
<Button variant="secondary" onClick={() => { handleShow() }}>Preview</Button>
}
{' '}
<Button variant="secondary" onClick={() => { savePollToGaia() }} disabled={isProcessing || pollObject?.status != "draft"}>
Save
</Button>
{' '}
<Button variant="secondary" onClick={() => { publishPoll() }} disabled={isProcessing || pollObject?.status != "draft"}>
Publish
</Button>
{' '}
{currentProgressMessage &&
<span>{currentProgressMessage}</span>
}
</Form>
:
<>Loading...</>
}
</div>
</Col>
</Row>
</Container>
{/* Preview popup */}
<PreviewComponent pollObject={pollObject} show={show} handleClose={handleClose} />
</>
);
}

View File

@@ -0,0 +1,19 @@
import { Modal } from "react-bootstrap";
import PollComponent from "../poll/PollComponent";
export default function PreviewComponent(props) {
const { show, handleClose, pollObject } = props;
return (
<>
<Modal size="xl" show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Preview</Modal.Title>
</Modal.Header>
<Modal.Body>
<PollComponent isPreview="true" pollObject={pollObject} resultsByPosition={{}} />
</Modal.Body>
</Modal>
</>
)
}

View File

@@ -0,0 +1,55 @@
import { formStacksExplorerUrl } from "../../common/utils";
export default function HeaderComponent(props) {
// Variables
const { pollObject } = props;
// Function
// Design
return (
<>
{pollObject && pollObject.id &&
<>
{/* Title */}
<h4>{pollObject?.title}</h4>
{/* Info Bar */}
<div style={{ fontSize: "14px" }}>
{pollObject?.status == "draft" ? "Draft" : "Active"} {' '}
{pollObject?.userStxAddress &&
<a target="_blank" rel="noreferrer" href={formStacksExplorerUrl(pollObject?.userStxAddress)}>
<span>
{pollObject?.userStxAddress?.substring(0, 10)} { }
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#0d6efd"
/>
</svg>
</span>
</a>
}
</div>
{/* Description */}
<div style={{ margin: "20px 0", whiteSpace: "pre-wrap" }}>
<h5>Description</h5>
<p>
{pollObject?.description}
</p>
</div>
</>
}
</>
);
}

View File

@@ -0,0 +1,132 @@
import { convertToDisplayDateFormat, formStacksExplorerUrl } from "../../common/utils";
export default function InformationComponent(props) {
// Variables
const { pollObject, resultsByOption } = props;
// Function
// Design
return (
<>
{pollObject && pollObject.id &&
<>
<div style={{ padding: "10px", border: "1px solid #cccccc", borderRadius: "5px", width: "100%" }}>
{/* Title */}
<h6>Information</h6>
<div style={{ marginTop: "10px" }}>
{
pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName &&
pollObject?.publishedInfo?.txId &&
<div>
Contract
<a target="_blank" rel="noreferrer" href={formStacksExplorerUrl(pollObject?.publishedInfo?.txId)}>
<span style={{ float: "right", fontWeight: "bold" }}>
{pollObject?.publishedInfo?.contractName.substring(0, 10)} { }
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#0d6efd"
/>
</svg>
</span>
</a>
</div>
}
{
pollObject?.ipfsLocation &&
<div>
IPFS
<a target="_blank" rel="noreferrer" href={`https://owllink.mypinata.cloud/ipfs/${pollObject?.ipfsLocation}`}>
<span style={{ float: "right", fontWeight: "bold" }}>
#{pollObject?.ipfsLocation.substring(0, 8)} { }
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#0d6efd"
/>
</svg>
</span>
</a>
</div>
}
{
pollObject?.strategyContractName &&
<div>
Strategy
<a target="_blank" rel="noreferrer" href={formStacksExplorerUrl(pollObject?.strategyContractName)}>
<span style={{ float: "right", fontWeight: "bold" }}>
{pollObject?.strategyContractName.substring(0, 10)} { }
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#0d6efd"
/>
</svg>
</span>
</a>
</div>
}
<div>
System <span style={{ float: "right", fontWeight: "bold", textTransform: "capitalize" }}>{pollObject?.votingSystem} Choice</span>
</div>
<div>
Start Date <span style={{ float: "right", fontWeight: "bold" }}>{convertToDisplayDateFormat(pollObject?.startAtDate)}</span>
</div>
<div>
End Date <span style={{ float: "right", fontWeight: "bold" }}>{convertToDisplayDateFormat(pollObject?.endAtDate)}</span>
</div>
{pollObject?.contractAddress &&
<div>
Contract Address <span style={{ float: "right", fontWeight: "bold" }}>{pollObject?.contractAddress}</span>
</div>
}
</div>
</div>
<div style={{ padding: "10px", marginTop: "10px", border: "1px solid #cccccc", borderRadius: "5px", width: "100%" }}>
{/* Title */}
<h6>Current results</h6>
<div style={{ marginTop: "10px" }}>
{pollObject?.options?.map((option, index) => (
<div key={index}>
{option?.value}
<span style={{ float: "right", fontWeight: "bold", textTransform: "capitalize" }}>
{resultsByOption && resultsByOption[option.id] ? resultsByOption[option.id] : "-"}
</span>
</div>
))}
</div>
</div>
</>
}
</>
);
}

View File

@@ -0,0 +1,121 @@
import { useEffect, useState } from "react";
import { Col, Container, Row, Button } from "react-bootstrap";
import styles from "../../styles/Dashboard.module.css";
import { getFileFromGaia, getUserData } from "../../services/auth.js"
import Link from "next/link";
import { convertToDisplayDateFormat } from "../../common/utils";
export default function DashboardAllPollsComponent() {
// Variables
// All polls
const [allPolls, setAllPolls] = useState();
// Functions
useEffect(() => {
getFileFromGaia("pollIndex.json", {}).then(
(response) => {
if (response) {
setAllPolls(JSON.parse(response));
}
},
(error) => {
// File does not exit in gaia
if (error && error.code == "does_not_exist") {
setAllPolls({
list: [],
ref: {}
});
}
});
}, []);
// Function
function getEachRow(pollObj) {
const gaiaAddress = getUserData()?.gaiaHubConfig?.address;
return (
<Link href={pollObj?.status == "draft" ? `/builder/${pollObj.id}/draft` : `/p/${pollObj.id}/${gaiaAddress}/results`}>
<div style={{ border: "1px solid black", borderRadius: "4px", padding: "10px", marginBottom: "10px", cursor: "pointer" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div style={{ fontSize: "16px", fontWeight: "bold" }}>
{pollObj?.title ? pollObj?.title : "..."}
</div>
<div style={{ fontSize: "14px" }}>
<span>
Status : {pollObj?.status == "draft" ? "Draft" : "Active"}
</span>
{', '}
<span>
Last Modified : {convertToDisplayDateFormat(pollObj?.updatedAt)}
</span>
</div>
</div>
{/*
<div>
{pollObj?.status == "draft" &&
<Link href={`/builder/${pollObj.id}/draft`}>
<Button style={{ marginRight: "5px" }}>
Edit
</Button>
</Link>
}
{pollObj?.status == "live" &&
<Link href={`/results/${pollObj.ipfsLocation}`}>
<Button style={{ marginRight: "5px" }}>
View
</Button>
</Link>
}
<Button>
Preview
</Button>
</div> */}
</div>
</div>
</Link>
)
}
// Design
return (
<>
<Container fluid>
<Row className="justify-content-md-center">
<Col lg={8} md={12}>
<div className={styles.dashboard_container}>
{/* Welcome */}
{/* Title */}
<h4>All Polls</h4>
{/* List of all polls */}
<div style={{ padding: "10px 0" }}>
{allPolls?.list && allPolls?.ref ?
allPolls?.list?.length > 0 ?
allPolls?.list.map(
(pollId, i) => (
<div key={i}>
{getEachRow(allPolls.ref[pollId])}
</div>
)
)
:
<>No data found</>
:
<>Loading...</>
}
</div>
</div>
</Col>
</Row>
</Container>
</>
);
}

View File

@@ -0,0 +1,33 @@
import Link from "next/link";
import { Container, Row, Col, Button } from "react-bootstrap";
export default function DashboardMenuComponent() {
return (
<>
<Container>
<Row>
<Col md={12} style={{ paddingTop: "10px" }}>
<h4>Ballot</h4>
<div style={{ margin: "10px 0" }}>
<Link href="/builder/new">
<Button>
+ New Poll
</Button>
</Link>
</div>
<div>
<Link href="/all-polls">
<Button>
All Polls
</Button>
</Link>
</div>
</Col>
</Row>
</Container>
</>
);
}

View File

@@ -0,0 +1,234 @@
import { useState } from "react";
import { Button, Col, Container, Form, Modal, Row, Table } from "react-bootstrap";
import { castMyVoteContractCall } from "../../common/contract";
import { formStacksExplorerUrl } from "../../common/utils";
import { authenticate, userSession } from "../../services/auth";
import styles from "../../styles/Poll.module.css";
import HeaderComponent from "../common/HeaderComponent";
import InformationComponent from "../common/InformationComponent";
export default function PollComponent(props) {
// Variables
// Poll id and Gaia address
const {
pollObject,
isPreview,
optionsMap,
resultsByOption,
resultsByPosition,
total,
dns,
alreadyVoted,
noHoldingToken,
holdingTokenArr,
holdingTokenIdArr,
votingPower } = props;
// Capture the vote
const [voteObject, setVoteObject] = useState({});
const [txId, setTxId] = useState();
// Show popup
const [show, setShow] = useState(false);
const handleShow = () => setShow(true);
const handleClose = () => setShow(false);
// Functions
const handleChange = e => {
const { name, value } = e.target;
if (pollObject?.votingSystem == "single") {
voteObject = {
[value]: votingPower
};
setVoteObject(voteObject);
} else {
if (voteObject?.[value]) {
delete voteObject[value];
} else {
voteObject[value] = votingPower;
}
}
};
const callbackFunction = (data) => {
if (data?.txId) {
setTxId(data.txId);
handleShow();
}
}
const castMyVote = () => {
if (pollObject?.publishedInfo?.contractAddress && pollObject?.publishedInfo?.contractName) {
const contractAddress = pollObject?.publishedInfo?.contractAddress;
const contractName = pollObject?.publishedInfo?.contractName;
castMyVoteContractCall(contractAddress, contractName, voteObject, dns, holdingTokenIdArr?.[0], callbackFunction);
}
}
// Design
return (
<>
<Container fluid>
<Row className="justify-content-md-center">
<Col lg={isPreview ? 12 : 8} md={12}>
<div className={styles.results_container}>
{pollObject && pollObject.id ?
<>
<div className="row">
{/* Left Side */}
<div className="col-sm-12 col-md-8">
{/* Header */}
<HeaderComponent pollObject={pollObject} />
{/* Cast your vote */}
<div style={{ marginTop: "20px" }}>
<h5>Cast your vote</h5>
<div>
<Form>
<Form.Group className="mb-3">
{pollObject?.options.map((option, index) => (
<Form.Check
key={index}
type={pollObject?.votingSystem == "single" ? "radio" : "checkbox"}
name="vote"
value={option.id}
label={option.value}
id={option.id}
onChange={handleChange}
/>
))}
{userSession && userSession.isUserSignedIn() ?
<Button style={{ marginTop: "10px" }}
disabled={(isPreview || !holdingTokenArr || alreadyVoted) ? true : false}
onClick={() => { castMyVote() }}>
Vote
</Button>
:
<Button style={{ marginTop: "10px" }}
onClick={() => { authenticate() }}>
Vote
</Button>
}
{/* Holdings Required */}
{noHoldingToken &&
<div style={{ fontSize: "14px", color: "red" }}>
Holdings required to vote this poll.
</div>
}
{/* Already voted */}
{alreadyVoted &&
<div style={{ fontSize: "14px", color: "red" }}>
You have already cast your vote.
</div>
}
</Form.Group>
</Form>
</div>
</div>
{/* Results */}
<div style={{ marginTop: "20px" }}>
<h5>Votes ({total})</h5>
<Table striped bordered hover>
<thead>
<tr>
<th>Address</th>
<th>Option</th>
<th>Voting Power</th>
</tr>
</thead>
<tbody>
{Object.keys(resultsByPosition).map((position, index) => (
<tr key={index}>
<td>{resultsByPosition[position]?.address &&
<a target="_blank" rel="noreferrer" href={formStacksExplorerUrl(resultsByPosition[position]?.address)}>
<span>
{resultsByPosition[position]?.address} { }
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#0d6efd"
/>
</svg>
</span>
</a>
}</td>
<td>
{Object.keys(resultsByPosition[position]?.vote).map((optionId, voteIndex) => (
<div key={voteIndex}>
{optionsMap[optionId] ? optionsMap[optionId] : "-"}
</div>
))}
</td>
<td>1</td>
</tr>
))}
</tbody>
</Table>
</div>
</div>
{/* Right Side */}
<div className="col-sm-12 col-md-4">
{/* Information */}
<InformationComponent pollObject={pollObject} resultsByOption={resultsByOption} />
</div>
</div>
</>
:
<>Loading...</>
}
</div>
</Col>
</Row>
</Container>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Information</Modal.Title>
</Modal.Header>
<Modal.Body>
Successfully! You have cast your vote. Check your transaction status on explorer
<a
style={{ textDecoration: "underline", color: "#000" }}
href={formStacksExplorerUrl(txId)}
target="_blank"
rel="noreferrer"
>
{" here "}
<svg
width="10"
height="10"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5044 0.743397C3.5044 0.33283 3.83723 -6.71395e-08 4.2478 0L11.2566 6.60206e-07C11.6672 6.60206e-07 12 0.33283 12 0.743397L12 7.7522C12 8.16277 11.6672 8.4956 11.2566 8.4956C10.846 8.4956 10.5132 8.16277 10.5132 7.7522V2.53811L1.26906 11.7823C0.978742 12.0726 0.50805 12.0726 0.217736 11.7823C-0.0725787 11.4919 -0.0725784 11.0213 0.217736 10.7309L9.46189 1.48679L4.2478 1.48679C3.83723 1.48679 3.5044 1.15396 3.5044 0.743397Z"
fill="#000"
/>
</svg>
</a>
</Modal.Body>
</Modal>
</>
);
}

7
next.config.js Normal file
View File

@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

1
node_modules/.bin/acorn generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../acorn/bin/acorn

1
node_modules/.bin/eslint generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../eslint/bin/eslint.js

1
node_modules/.bin/js-yaml generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../js-yaml/bin/js-yaml.js

1
node_modules/.bin/json5 generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../json5/lib/cli.js

1
node_modules/.bin/loose-envify generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../loose-envify/cli.js

1
node_modules/.bin/nanoid generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../nanoid/bin/nanoid.js

1
node_modules/.bin/next generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../next/dist/bin/next

1
node_modules/.bin/node-which generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../which/bin/node-which

1
node_modules/.bin/prettier generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../prettier/bin-prettier.js

1
node_modules/.bin/resolve generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../resolve/bin/resolve

1
node_modules/.bin/rimraf generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../rimraf/bin.js

1
node_modules/.bin/semver generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../semver/bin/semver.js

1
node_modules/.bin/sha.js generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../sha.js/bin.js

1
node_modules/.bin/stencil generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@stencil/core/bin/stencil

1
node_modules/.bin/uuid generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../uuid/dist/bin/uuid

3944
node_modules/.package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

22
node_modules/@babel/runtime-corejs3/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
node_modules/@babel/runtime-corejs3/README.md generated vendored Normal file
View File

@@ -0,0 +1,19 @@
# @babel/runtime-corejs3
> babel's modular runtime helpers with core-js@3 polyfilling
See our website [@babel/runtime-corejs3](https://babeljs.io/docs/en/babel-runtime-corejs3) for more information.
## Install
Using npm:
```sh
npm install --save @babel/runtime-corejs3
```
or using yarn:
```sh
yarn add @babel/runtime-corejs3
```

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/array/from");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/array/is-array");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/array/of");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/clear-immediate");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/date/now");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/bind");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/code-point-at");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/concat");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/copy-within");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/ends-with");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/entries");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/every");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/fill");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/filter");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/find-index");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/find");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/flags");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/flat-map");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/flat");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/for-each");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/includes");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/index-of");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/keys");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/last-index-of");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/map");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/pad-end");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/pad-start");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/reduce-right");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/reduce");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/repeat");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/reverse");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/slice");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/some");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/sort");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/splice");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/starts-with");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/trim-end");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/trim-left");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/trim-right");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/trim-start");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/trim");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/instance/values");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/json/stringify");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/map");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/acosh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/asinh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/atanh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/cbrt");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/clz32");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/cosh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/expm1");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/fround");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/hypot");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/imul");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/log10");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/log1p");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/log2");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/sign");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/sinh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/tanh");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/math/trunc");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/epsilon");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/is-finite");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/is-integer");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/is-nan");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/is-safe-integer");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/max-safe-integer");

View File

@@ -0,0 +1 @@
module.exports = require("core-js-pure/stable/number/min-safe-integer");

Some files were not shown because too many files have changed in this diff Show More