lesson-3_仕上げを実装して完成させよう
コントラクトの関数を呼び出して election-dApp を完成させよう
ではいよいよコントラクトで作成した関数を呼び出して投票アプリを完成させまし ょう!
まずはpages/voter.js
に移動して下のように書き換えましょう。
[voter.js]
// 以下のように書き換えましょう
import React, { useState } from "react";
import Title from "../components/title";
import Input from "../components/input_form";
import { nft_mint, check_voter_has_been_added } from "../js/near/utils";
// Adding voter screen
const Voter = () => {
// valuable of input ID for receiving vote ticket
const [inputId, setInputId] = useState("");
// mint function
const mint = async () => {
// check if user is deployer
if (window.accountId !== process.env.CONTRACT_NAME) {
alert("You are not contract deployer, so you can't add voter");
return;
}
// check if a ticket minted to user before
const isMinted = await check_voter_has_been_added(`${inputId}`);
if (isMinted !== 0) {
alert("You've already got vote ticket or voted and used it!");
return;
}
// mint vote ticket to user
await nft_mint(
"Vote Ticket",
"",
"https://gateway.pinata.cloud/ipfs/QmUs5K3LwdvbhKA58bH9C6FX5Q7Bhsvvg9GRAhr9aVKLyx",
"QmUs5K3LwdvbhKA58bH9C6FX5Q7Bhsvvg9GRAhr9aVKLyx",
"Vote Ticket",
"You can vote with this ticket! But remember that you can do it just once.",
"vote",
`${inputId}`
);
alert(`Vote ticket is minted to ${inputId}!`);
setInputId("");
};
return (
<div className="grid place-items-center w-full">
<Title name="Add Voter" />
<div className="text-lg">※Only contract deployer can add voter.</div>
<div className="mb-24"></div>
<Input
title="Wallet ID"
hint="0x..."
input={inputId}
type="text"
setInput={(event) => setInputId(event.target.value)}
/>
<div className="mb-24"></div>
<button className="button" onClick={() => mint()}>
Add
</button>
</div>
);
};
export default Voter;
まずnft_mint, check_voter_has_been_added
の2つの関数をインポートしましょう。
import { nft_mint, check_voter_has_been_added } from "../js/near/utils";
この部分ではuseState
を使って入力値を取得できるようにします。
const [inputId, setInputId] = useState("");
次にmint関数
の中身を確認してみましょう。
まず操作しているユーザーがコントラクトをdeployした人かどうかを確認します。
その次にcheck_voter_has_been_added
を呼び出して、すでに投票券を付与していないかを確認します。
if (window.accountId !== process.env.CONTRACT_NAME) {
alert("You are not contract deployer, so you can't add voter");
return;
}
// check if a ticket minted to user before
const isMinted = await check_voter_has_been_added(`${inputId}`);
if (isMinted !== 0) {
alert("You've already got vote ticket or voted and used it!");
return;
}
これら2つをクリアした場合にnft_mint関数
を呼び出して、投票券を入力されたWallet Idが所有者になるようにmintして、mint後にユーザーにmintされたことを表示します。
最後に入力フォームを空にします。
await nft_mint(
"Vote Ticket",
"",
"https://gateway.pinata.cloud/ipfs/QmUs5K3LwdvbhKA58bH9C6FX5Q7Bhsvvg9GRAhr9aVKLyx",
"QmUs5K3LwdvbhKA58bH9C6FX5Q7Bhsvvg9GRAhr9aVKLyx",
"Vote Ticket",
"You can vote with this ticket! But remember that you can do it just once.",
"vote",
`${inputId}`
);
alert(`Vote ticket is minted to ${inputId}!`);
setInputId("");
描画するUIはreturnする以下のものです。
一番下のボタンを押した時にmint関数
が走るようになっています。
<div className="grid place-items-center w-full">
<Title name="Add Voter" />
<div className="text-lg">※Only contract deployer can add voter.</div>
<div className="mb-24"></div>
<Input
title="Wallet ID"
hint="0x..."
input={inputId}
type="text"
setInput={(event) => setInputId(event.target.value)}
/>
<div className="mb-24"></div>
<button className="button" onClick={() => mint()}>
Add
</button>
</div>
これでAdd Voter画面
は完成です。
次にpages/candidate.js
に移動して以下のように書き換えてください。
[candidate.js]
// 以下のように書き換えましょう
import React, { useState } from "react";
import Title from "../components/title";
import Input from "../components/input_form";
import { nft_mint } from "../js/near/utils";
// add candidate screen
function Candidate() {
// set input valuable of candidate image CID, candidate name, candidate manifest
const [inputCID, setInputCID] = useState("");
const [inputName, setInputName] = useState("");
const [inputManifest, setInputManifest] = useState("");
// function that add candidate info to home screen
const addCandidate = async () => {
// mint candidate nft
await nft_mint(
`${inputName}(candidate)`,
"",
`https://gateway.pinata.cloud/ipfs/${inputCID}`,
inputCID,
inputName,
inputManifest,
"candidate",
process.env.CONTRACT_NAME
);
setInputCID("");
setInputName("");
setInputManifest("");
alert("Candidate's NFT has minted! Let's Check it at Home screen!");
};
return (
<div className="grid place-items-center w-full">
<Title name="Add Candidate" />
<div className="my-3 text-2xl text-red-400">
Add candidate who you think must be a leader!
</div>
<Input
title="Image URI(IPFS Content CID)"
hint="QmT..."
className="mb-3"
input={inputCID}
setInput={(event) => setInputCID(event.target.value)}
/>
<div className="mb-6"></div>
<Input
title="Name"
hint="Robert Downey Jr."
input={inputName}
setInput={(event) => setInputName(event.target.value)}
/>
<div className="mb-6"></div>
<Input
title="Manifest"
hint="I'm gonna prosper this city with web3 tech!"
input={inputManifest}
setInput={(event) => setInputManifest(event.target.value)}
/>
<div className="mb-6"></div>
<button className="button" onClick={async () => addCandidate()}>
Add
</button>
</div>
);
}
export default Candidate;
まずnft_mint関数
をインポートしています。
import { nft_mint } from "../js/near/utils";
addCandidate関数
では入力されたCIDや候補者の名前を取得して候補者NFTをmintします。
const addCandidate = async () => {
// mint candidate nft
await nft_mint(
`${inputName}(candidate)`,
"",
`https://gateway.pinata.cloud/ipfs/${inputCID}`,
inputCID,
inputName,
inputManifest,
"candidate",
process.env.CONTRACT_NAME
);
setInputCID("");
setInputName("");
setInputManifest("");
alert("Candidate's NFT has minted! Let's Check it at Home screen!");
};
最後にreturn内がUIとなります。
<div className="grid place-items-center w-full">
<Title name="Add Candidate" />
<div className="my-3 text-2xl text-red-400">
Add candidate who you think must be a leader!
</div>
<Input
title="Image URI(IPFS Content CID)"
hint="QmT..."
className="mb-3"
input={inputCID}
setInput={(event) => setInputCID(event.target.value)}
/>
<div className="mb-6"></div>
<Input
title="Name"
hint="Robert Downey Jr."
input={inputName}
setInput={(event) => setInputName(event.target.value)}
/>
<div className="mb-6"></div>
<Input
title="Manifest"
hint="I'm gonna prosper this city with web3 tech!"
input={inputManifest}
setInput={(event) => setInputManifest(event.target.value)}
/>
<div className="mb-6"></div>
<button className="button" onClick={async () => addCandidate()}>
Add
</button>
</div>
これでAdd Candidate画面
は完成です。
最後にpages/home.js
に移動して下のように編集しましょう。
[home.js]
// 以下のように書き換えましょう
import React, { useEffect, useState } from "react";
import {
nft_transfer,
nft_add_likes_to_candidate,
nft_tokens_for_kind,
nft_return_candidate_likes,
check_voter_has_been_added,
check_voter_has_voted,
voter_voted,
close_election,
if_election_closed,
reopen_election,
} from "../js/near/utils";
import CandidateCard from "../components/candidate_card";
import LikeIcon from "../img/like_icon.png";
// Home screen(user can vote here)
const Home = () => {
// set valuable for candidate NFT info, num of likes for each candidate, state
const [candidateInfoList, setCandidateInfoList] = useState();
const [candidateLikesList] = useState([]);
const [state, setState] = useState("fetching");
// enum of state
const State = {
Fetching: "fetching",
Fetched: "fetched",
Open: "open",
Closed: "closed",
};
// fetch candidate nft info
useEffect(async () => {
await nft_tokens_for_kind("candidate").then((value) => {
setCandidateInfoList(value);
setState("fetched");
});
}, []);
// vote function
const vote = (token_id) => {
//check if user has already voted
check_voter_has_voted(window.accountId).then((value) => {
if (Boolean(value)) {
alert("You have already voted!");
return;
}
// check if user has vote ticket
check_voter_has_been_added(window.accountId).then((value) => {
let tokenIdOfVoter = parseFloat(value);
if (tokenIdOfVoter == 0) {
alert(
"You don't have vote ticket! Please ask deployer to give it to you."
);
return;
}
// confirm if user really vote to specified candidate(because even if they cancel transaction, contract judge user voted)
let isSure = confirm(
"Once you vote, you can't change selected candidate. Are you OK?"
);
if (!isSure) {
return;
}
// transfer vote ticket from user to contract(get rid of vote ticket)
nft_transfer(process.env.CONTRACT_NAME, tokenIdOfVoter);
// add vote to specified candidate
nft_add_likes_to_candidate(token_id);
//add user ID to voted-list
voter_voted(window.accountId);
});
});
};
// body(in case election is open)
const cardsInCaseOpen = () => {
let candidateCardList = [];
for (let i = 0; i < candidateInfoList.length; i++) {
// format data for rendering
candidateCardList.push(
<div className="items-center">
<CandidateCard
CID={candidateInfoList[i].metadata.media_CID}
name={candidateInfoList[i].metadata.candidate_name}
manifest={candidateInfoList[i].metadata.candidate_manifest}
/>
<div className="center text-xl items-center">
<img src={LikeIcon} className="object-cover h-5 w-5 mr-2" />
<p className="mr-2">{candidateLikesList[i]}</p>
<button
value={candidateInfoList[i].metadata.token_id}
onClick={(event) => vote(parseInt(event.target.value))}
className="vote_button hover:skew-1"
>
Vote!
</button>
</div>
</div>
);
}
return candidateCardList;
};
// body(in case election is closed)
const cardsInCaseClosed = () => {
let candidateCardList = [];
let mostVotedNum = candidateLikesList.reduce((a, b) => {
return Math.max(a, b);
});
// format data for rendering
for (let i = 0; i < candidateInfoList.length; i++) {
if (candidateLikesList[i] == mostVotedNum) {
// for winner candidate rendering
candidateCardList.push(
<div className="items-center">
<div className="text-2xl shadow-rose-600 center font-semibold text-red-700">
Won!
</div>
<CandidateCard
CID={candidateInfoList[i].metadata.media_CID}
name={candidateInfoList[i].metadata.candidate_name}
manifest={candidateInfoList[i].metadata.candidate_manifest}
/>
<div className="center text-xl items-center">
<img src={LikeIcon} className="object-cover h-5 w-5 mr-2" />
<p className="mr-2">{candidateLikesList[i]}</p>
</div>
</div>
);
} else {
// for other candidate rendering
candidateCardList.push(
<div className="items-center opacity-20">
<div className="pt-7"></div>
<CandidateCard
CID={candidateInfoList[i].metadata.media_CID}
name={candidateInfoList[i].metadata.candidate_name}
manifest={candidateInfoList[i].metadata.candidate_manifest}
/>
<div className="center text-xl items-center">
<img src={LikeIcon} className="object-cover h-5 w-5 mr-2" />
<p className="mr-2">{candidateLikesList[i]}</p>
</div>
</div>
);
}
}
return candidateCardList;
};
// fetching like method
const getCandidateLikes = async () => {
// get num of likes for each candidate
for (let i = 0; i < candidateInfoList.length; i++) {
await nft_return_candidate_likes(
candidateInfoList[i].metadata.token_id
).then((value) => {
candidateLikesList.push(value);
});
}
// check if election is closed
let isClosed = await if_election_closed();
console.log(isClosed);
if (isClosed) {
setState("closed");
} else {
setState("open");
}
};
// close button function(display to only contract deployer)
const closeButton = () => {
// check if user is contract deployer
if (window.accountId !== process.env.CONTRACT_NAME) {
return;
}
return (
<button
className="close_button hover:skew-1 h-10 bg-red-600 mb-3"
onClick={() => {
// confirm that user really close this election
let isSureToClose = confirm("Are you sure to close this election?");
if (isSureToClose) {
// close this election
close_election();
// change state to closed
setState("closed");
}
}}
>
Close Election
</button>
);
};
// reopen button function(display to only contract deployer)
const reopenButton = () => {
// check if user is contract deployer
if (window.accountId !== process.env.CONTRACT_NAME) {
return;
}
return (
<button
className="close_button hover:skew-1 h-10 bg-red-600 mb-3"
onClick={() => {
let isSureToClose = confirm("Are you sure to reopen this election?");
if (isSureToClose) {
// reopen this election
reopen_election();
// change state to open
setState("open");
}
}}
>
Reopen Election
</button>
);
};
// message to wait for fetching data
const messageToWait = () => {
return (
<div className="grid h-screen place-items-center text-3xl">
Fetching NFTs of candidates...
</div>
);
};
switch (state) {
// in case fetching candidate NFTs info
case State.Fetching:
return <div>{messageToWait()}</div>;
// in case fetching number of likes for each candidate
case State.Fetched:
getCandidateLikes();
return <div>{messageToWait()}</div>;
// in case all data is fetched(election is open)
case State.Open:
return (
<div>
<div className="center">{closeButton()}</div>
<div className="grid grid-cols-3 gap-10">{cardsInCaseOpen()}</div>
</div>
);
// in case all data is fetched(election is closed)
case State.Closed:
return (
<div>
<div className="center">{reopenButton()}</div>
<div className="grid grid-cols-3 gap-10">{cardsInCaseClosed()}</div>
</div>
);
}
};
export default Home;
このようにコントラクトから関数をインポートします。
import {
nft_transfer,
nft_add_likes_to_candidate,
nft_tokens_for_kind,
nft_return_candidate_likes,
check_voter_has_been_added,
check_voter_has_voted,
voter_voted,
close_election,
if_election_closed,
reopen_election,
} from "../js/near/utils";
次に更新する変数を定義します。candidateInfoList
は候補者の情報を入れるリスト、candidateLikesList
はそれぞれの候補者の得票数を入れるリスト、state
はデータの取得・投票の締切の真偽の状態を管理するための変数です。
const [candidateInfoList, setCandidateInfoList] = useState();
const [candidateLikesList] = useState([]);
const [state, setState] = useState("fetching");