diff --git a/.DS_Store b/.DS_Store index 8ef6025e..803fc4ad 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/client/client/pages/_app.jsx b/client/client/pages/_app.jsx index 14e3a3de..6b6c1fc3 100644 --- a/client/client/pages/_app.jsx +++ b/client/client/pages/_app.jsx @@ -1,7 +1,8 @@ import '../styles/globals.css'; +import React from 'react'; // Imports for proposals/voting section -import { ProposalDetails, CreateProposal, Home, Profile } from './proposals'; +import { ProposalDetails, CreateProposal, Home, Profile } from './proposals/Home'; function MyApp({ Component, pageProps }) { return diff --git a/client/client/pages/index.js b/client/client/pages/index.js index f2365a14..dc4b6403 100644 --- a/client/client/pages/index.js +++ b/client/client/pages/index.js @@ -1,6 +1,6 @@ -import Head from 'next/head'; -import Image from 'next/image'; -import styles from '../styles/Home.module.css'; +import Head from 'next/head' +import Image from 'next/image' +import styles from '../styles/Home.module.css' export default function Home() { return ( diff --git a/client/client/pages/proposals/Home.jsx b/client/client/pages/proposals/Home.jsx new file mode 100644 index 00000000..12c9a283 --- /dev/null +++ b/client/client/pages/proposals/Home.jsx @@ -0,0 +1,32 @@ +/*export { default as Home } from './Home'; +export { default as Profile } from './Profile'; +export { default as CreateProposal } from './CreateProposal'; +export { default as ProposalDetails } from './ProposalDetails';*/ + +import React, { useState, useEffect } from "react"; +import { useStateContext } from './context'; +//import { DisplayProposals } from './components'; + +const Home = () => { + const [isLoading, setIsLoading] = useState(false); + const [proposals, setProposals] = useState([]); // Empty array, retrieved from the state context from onchain + + const { address, contract, getProposals } = useStateContext(); /* + const fetchProposals = async () => { // This is to allow us to call this g.request in the useEffect (as the request is async in /context) + setIsLoading(true); + const data = await getProposals(); + setProposals(data); + setIsLoading(false); + } + + useEffect(() => { + if (contract) fetchProposals(); + console.log(proposals); + }, [address, contract]); // Re-called when these change*/ + + return ( +
Hello World
+ ) +} + +export default Home; \ No newline at end of file diff --git a/client/client/pages/proposals/assets/create-campaign.svg b/client/client/pages/proposals/assets/create-campaign.svg new file mode 100644 index 00000000..d9c67303 --- /dev/null +++ b/client/client/pages/proposals/assets/create-campaign.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/client/client/pages/proposals/assets/dashboard.svg b/client/client/pages/proposals/assets/dashboard.svg new file mode 100644 index 00000000..b9ecf4cf --- /dev/null +++ b/client/client/pages/proposals/assets/dashboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/client/pages/proposals/assets/index.js b/client/client/pages/proposals/assets/index.js new file mode 100644 index 00000000..49dff30b --- /dev/null +++ b/client/client/pages/proposals/assets/index.js @@ -0,0 +1,31 @@ +import createCampaign from './create-campaign.svg'; +import dashboard from './dashboard.svg'; +import logo from './logo.svg'; +import logout from './logout.svg'; +import payment from './payment.svg'; +import profile from './profile.svg'; +import sun from './sun.svg'; +import withdraw from './withdraw.svg'; +import tagType from './type.svg'; +import search from './search.svg'; +import menu from './menu.svg'; +import money from './money.svg'; +import loader from './loader.svg'; +import thirdweb from './thirdweb.png'; + +export { + tagType, + createCampaign, + dashboard, + logo, + logout, + payment, + profile, + sun, + withdraw, + search, + menu, + money, + loader, + thirdweb, +}; diff --git a/client/client/pages/proposals/assets/loader.svg b/client/client/pages/proposals/assets/loader.svg new file mode 100644 index 00000000..476b2dd4 --- /dev/null +++ b/client/client/pages/proposals/assets/loader.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/client/pages/proposals/assets/logo.svg b/client/client/pages/proposals/assets/logo.svg new file mode 100644 index 00000000..ad1ca4ba --- /dev/null +++ b/client/client/pages/proposals/assets/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/client/pages/proposals/assets/logout.svg b/client/client/pages/proposals/assets/logout.svg new file mode 100644 index 00000000..188cf3b3 --- /dev/null +++ b/client/client/pages/proposals/assets/logout.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/client/pages/proposals/assets/menu.svg b/client/client/pages/proposals/assets/menu.svg new file mode 100644 index 00000000..4685dfbc --- /dev/null +++ b/client/client/pages/proposals/assets/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/client/pages/proposals/assets/money.svg b/client/client/pages/proposals/assets/money.svg new file mode 100644 index 00000000..8bf7f8e5 --- /dev/null +++ b/client/client/pages/proposals/assets/money.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/client/pages/proposals/assets/payment.svg b/client/client/pages/proposals/assets/payment.svg new file mode 100644 index 00000000..b0623289 --- /dev/null +++ b/client/client/pages/proposals/assets/payment.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/client/pages/proposals/assets/profile.svg b/client/client/pages/proposals/assets/profile.svg new file mode 100644 index 00000000..0558003e --- /dev/null +++ b/client/client/pages/proposals/assets/profile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/client/pages/proposals/assets/search.svg b/client/client/pages/proposals/assets/search.svg new file mode 100644 index 00000000..7155623f --- /dev/null +++ b/client/client/pages/proposals/assets/search.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/client/client/pages/proposals/assets/sun.svg b/client/client/pages/proposals/assets/sun.svg new file mode 100644 index 00000000..89ed57d6 --- /dev/null +++ b/client/client/pages/proposals/assets/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/client/pages/proposals/assets/thirdweb.png b/client/client/pages/proposals/assets/thirdweb.png new file mode 100644 index 00000000..2c64711f Binary files /dev/null and b/client/client/pages/proposals/assets/thirdweb.png differ diff --git a/client/client/pages/proposals/assets/type.svg b/client/client/pages/proposals/assets/type.svg new file mode 100644 index 00000000..f8eac581 --- /dev/null +++ b/client/client/pages/proposals/assets/type.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/client/pages/proposals/assets/withdraw.svg b/client/client/pages/proposals/assets/withdraw.svg new file mode 100644 index 00000000..29d8019b --- /dev/null +++ b/client/client/pages/proposals/assets/withdraw.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/client/client/pages/proposals/components/DisplayProposals.jsx b/client/client/pages/proposals/components/DisplayProposals.jsx new file mode 100644 index 00000000..09c40703 --- /dev/null +++ b/client/client/pages/proposals/components/DisplayProposals.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +//import { useNavigate } from 'react-router-dom'; +//import FundCard from './FundCard'; +import { loader } from '../assets'; + +const DisplayProposals = ({ title, isLoading, proposals }) => { + //const navigate = useNavigate(); + + const handleNavigate = (proposal) => { + navigate(`/proposal-details/${proposal.title}`, { state: proposal }) + } + + return ( +
+

{title} ({proposals.length})

+ +
+ {isLoading && ( + loader + )} + + {!isLoading && proposals.length === 0 && ( +

+ You have not created any proposals yet +

+ )} + + {!isLoading && proposals.length > 0 && proposals.map((proposal) => handleNavigate(proposal)} + />)} +
+
+ ) +} + +export default DisplayProposals; \ No newline at end of file diff --git a/client/client/pages/proposals/components/Sidebar.jsx b/client/client/pages/proposals/components/Sidebar.jsx new file mode 100644 index 00000000..994c0436 --- /dev/null +++ b/client/client/pages/proposals/components/Sidebar.jsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { logo, sun } from '../assets'; +import { navlinks } from '../constants'; +//import { Link } from 'next'; +import { Link, useNavigate } from 'react-router-dom'; + + +const Icon = ({ styles, name, imgUrl, isActive, disabled, handleClick }) => ( +
+ {!isActive ? ( + fund_logo + ) : ( + fund_logo + )} +
+); + +const Sidebar = () => { + const navigate = useNavigate(); + const [isActive, setIsActive] = useState('dashboard'); + + return ( +
+ + + + +
+
+ {navlinks.map((link) => ( + { + if(!link.disabled) { + setIsActive(link.name); + navigate(link.link); + } + }} + /> + ))} +
+ +
+
+ ); +} + +export default Sidebar; \ No newline at end of file diff --git a/client/client/pages/proposals/constants/index.js b/client/client/pages/proposals/constants/index.js new file mode 100644 index 00000000..555e82a3 --- /dev/null +++ b/client/client/pages/proposals/constants/index.js @@ -0,0 +1,37 @@ +import { createCampaign, dashboard, logout, payment, profile, withdraw } from '../assets'; + +export const navlinks = [ + { + name: 'dashboard', + imgUrl: dashboard, + link: '/', + }, + { + name: 'campaign', + imgUrl: createCampaign, + link: '/create-proposal', + }, + { + name: 'payment', + imgUrl: payment, + link: '/', + disabled: true, + }, + { + name: 'withdraw', + imgUrl: withdraw, + link: '/', + disabled: true, + }, + { + name: 'profile', + imgUrl: profile, + link: '/profile', + }, + { + name: 'logout', + imgUrl: logout, + link: '/', + disabled: true, + }, +]; \ No newline at end of file diff --git a/client/client/pages/proposals/context/index.jsx b/client/client/pages/proposals/context/index.jsx new file mode 100644 index 00000000..71cc1c7e --- /dev/null +++ b/client/client/pages/proposals/context/index.jsx @@ -0,0 +1,97 @@ +import React, { useContext, createContext } from 'react'; +import { useAddress, useContract, useMetamask, useContractWrite } from '@thirdweb-dev/react'; +import { ethers } from 'ethers'; + +const StateContext = createContext(); + +export const StateContextProvider = ({ children }) => { + const { contract } = useContract('0xCcaA1ABA77Bae6296D386C2F130c46FEc3E5A004'); + const { mutateAsync: createProposal } = useContractWrite(contract, 'createProposal'); // Call function & create a proposal, passing in params from the form + const address = useAddress(); + const connect = useMetamask(); + + // Publish a proposal on-chain + const publishProposal = async (form) => { + try { + const data = await createProposal([ + address, // Owner - creator of the campaign. useMetamask(); + form.title, // From CreateProposal.jsx + form.description, + form.target, + new Date(form.deadline).getTime(), + form.image, + ]); + + console.log("Contract call success: ", data); + } catch (error) { + console.error('Contract call resulted in a failure, ', error); + } + } + + // Retrieve proposals from on-chain + const getProposals = async () => { + const proposals = await contract.call('getProposals'); // Essentially a get request to the contract + const parsedProposals = proposals.map((proposal) => ({ // Take an individual proposal, immediate return + owner: proposal.owner, + title: proposal.title, + description: proposal.description, + target: ethers.utils.formatEther(proposal.target.toString()), + deadline: proposal.deadline.toNumber(), // Will transform to date format later + amountCollected: ethers.utils.formatEther(proposal.amountCollected.toString()), + image: proposal.image, + pId: proposal.i, // Index of proposal + })); + + console.log(parsedProposals); + console.log(proposals); + return parsedProposals; // This is sent to the `useEffect` in `Home.jsx` page + } + + const getUserProposals = async () => { // Get proposals that a specific user (authed) has created + const allProposals = await getProposals(); + const filteredProposals = allProposals.filter((proposal) => + proposal.owner === address + ); + return filteredProposals; + } + + const vote = async (pId, amount) => { + const data = await contract.call('voteForProposal', pId, { value: ethers.utils.parseEther(amount) }); + + return data; + } + + const getVotes = async (pId) => { + const votes = await contract.call('getVoters', pId); + const numberOfVotes = votes[0].length; + const parsedVotes = []; + + for (let i = 0; i < numberOfVotes; i++) { + parsedVotes.push({ + donator: donations[0][i], + donation: ethers.utils.formatEther(donations[1][i].toString) + }) + } + + return parsedVotes; + } + + return( + + {children} + + ) +} + +// Hook to get the context returned to node frontend +export const useStateContext = () => useContext(StateContext); \ No newline at end of file diff --git a/client/client/pages/proposals/index.js b/client/client/pages/proposals/index.js deleted file mode 100644 index eb9a1f19..00000000 --- a/client/client/pages/proposals/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { default as Home } from './Home'; -export { default as Profile } from './Profile'; -export { default as CreateProposal } from './CreateProposal'; -export { default as ProposalDetails } from './ProposalDetails'; \ No newline at end of file diff --git a/client/client/pages/proposals/utils/index.js b/client/client/pages/proposals/utils/index.js new file mode 100644 index 00000000..bf689a56 --- /dev/null +++ b/client/client/pages/proposals/utils/index.js @@ -0,0 +1,22 @@ +export const daysLeft = (deadline) => { + const difference = new Date(deadline).getTime() - Date.now(); + const remainingDays = difference / (1000 * 3600 * 24); + + return remainingDays.toFixed(0); + }; + + export const calculateBarPercentage = (goal, raisedAmount) => { + const percentage = Math.round((raisedAmount * 100) / goal); + + return percentage; + }; + + export const checkIfImage = (url, callback) => { + const img = new Image(); + img.src = url; + + if (img.complete) callback(true); + + img.onload = () => callback(true); + img.onerror = () => callback(false); + }; \ No newline at end of file diff --git a/client/client/postcss.config.js b/client/client/postcss.config.js new file mode 100644 index 00000000..56dcb488 --- /dev/null +++ b/client/client/postcss.config.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } + \ No newline at end of file diff --git a/client/client/tailwind.config.js b/client/client/tailwind.config.js new file mode 100644 index 00000000..16506015 --- /dev/null +++ b/client/client/tailwind.config.js @@ -0,0 +1,18 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + fontFamily: { + epilogue: ['Epilogue', 'sans-serif'], + }, + boxShadow: { + secondary: '10px 10px 20px rgba(2, 2, 2, 0.25)', + }, + }, + }, + plugins: [], + } \ No newline at end of file diff --git a/client/src/components/Content/Navbar.js b/client/src/components/Content/Navbar.js new file mode 100644 index 00000000..73f380bf --- /dev/null +++ b/client/src/components/Content/Navbar.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const Navbar = () => { + return ( +
Navbar
+ ) +} + +export default Navbar; \ No newline at end of file diff --git a/client/src/components/Content/README.md b/client/src/components/Content/README.md new file mode 100644 index 00000000..34a41683 --- /dev/null +++ b/client/src/components/Content/README.md @@ -0,0 +1 @@ +Components relating to the Lens Protocol UI elements \ No newline at end of file diff --git a/client/src/components/DisplayProposals.jsx b/client/src/components/DisplayProposals.jsx index 9dd78ad1..d9afb831 100644 --- a/client/src/components/DisplayProposals.jsx +++ b/client/src/components/DisplayProposals.jsx @@ -1,35 +1,38 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { loader } from '../assets'; import FundCard from './FundCard'; +import { loader } from '../assets'; const DisplayProposals = ({ title, isLoading, proposals }) => { - const navigate = useNavigate(); + const navigate = useNavigate(); + + const handleNavigate = (proposal) => { + navigate(`/proposal-details/${proposal.title}`, { state: proposal }) + } + + return ( +
+

{title} ({proposals.length})

+ +
+ {isLoading && ( + loader + )} - const handleNavigate = (proposal) => { - navigate(`/proposal-details/${proposal.title}`, { state: proposal }) - } + {!isLoading && proposals.length === 0 && ( +

+ You have not created any proposals yet +

+ )} - return ( -
-

{title}: ({proposals.length})

-
- {isLoading && ( - loader - )} - {!isLoading && proposals.length === 0 && ( // If there are no proposals matching the query -

- There are no proposals matching this query -

- )} - {!isLoading && proposals.length > 0 && proposals.map((proposal) => handleNavigate(proposal)} - />)} -
-
- ) + {!isLoading && proposals.length > 0 && proposals.map((proposal) => handleNavigate(proposal)} + />)} +
+
+ ) } export default DisplayProposals; \ No newline at end of file diff --git a/client/src/context/index.jsx b/client/src/context/index.jsx index acd11795..71cc1c7e 100644 --- a/client/src/context/index.jsx +++ b/client/src/context/index.jsx @@ -55,6 +55,27 @@ export const StateContextProvider = ({ children }) => { return filteredProposals; } + const vote = async (pId, amount) => { + const data = await contract.call('voteForProposal', pId, { value: ethers.utils.parseEther(amount) }); + + return data; + } + + const getVotes = async (pId) => { + const votes = await contract.call('getVoters', pId); + const numberOfVotes = votes[0].length; + const parsedVotes = []; + + for (let i = 0; i < numberOfVotes; i++) { + parsedVotes.push({ + donator: donations[0][i], + donation: ethers.utils.formatEther(donations[1][i].toString) + }) + } + + return parsedVotes; + } + return( { createProposal: publishProposal, getProposals, getUserProposals, + vote, + getVotes, }} > {children} diff --git a/client/src/pages/ProposalDetails.jsx b/client/src/pages/ProposalDetails.jsx index c0dfa1b3..e22adf15 100644 --- a/client/src/pages/ProposalDetails.jsx +++ b/client/src/pages/ProposalDetails.jsx @@ -8,7 +8,7 @@ import { thirdweb } from '../assets'; const ProposalDetails = () => { const { state } = useLocation(); - const { getVoters, contract, address } = useStateContext(); + const { vote, getVotes, contract, address } = useStateContext(); console.log(state); const [isLoading, setIsLoading] = useState(false); @@ -16,6 +16,24 @@ const ProposalDetails = () => { const [voters, setVoters] = useState([]); // Array of voters on a specific proposal const remainingDays = daysLeft(state.deadline); + const fetchVoters = async () => { + const data = await getVotes(state.pId); + setVoters(data); + } + + useEffect(() => { + if(contract) fetchVoters(); + }, [contract, address]); + + const handleVote = async () => { + setIsLoading(true); + + await vote(state.pId, amount); + + navigate('/') + setIsLoading(false); + } + return (
{isLoading && 'Loading...'} @@ -66,6 +84,31 @@ const ProposalDetails = () => {
+
+

Votes

+
+

Vote for this proposal

+
+ setAmount(e.target.value)} + /> +
+

Vote for this proposal with NO comments or adjustments

+
+ +
+
+
) diff --git a/tsconfig.json b/tsconfig.json index 84f59a7a..8aece21b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "module": "esnext", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "moduleResolution": "node" }, "include": [ "next-env.d.ts",