first commit
|
@ -1,13 +1,18 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.87caf152.css",
|
||||
"main.js": "/static/js/main.63db921f.js",
|
||||
"main.css": "/static/css/main.82b44b3f.css",
|
||||
"main.js": "/static/js/main.1ed44755.js",
|
||||
"static/media/gambar2.png": "/static/media/gambar2.7b305f6850ce809a4b77.png",
|
||||
"static/media/gambar1.png": "/static/media/gambar1.296084dbca65a169b9ef.png",
|
||||
"static/media/crowd.png": "/static/media/crowd.8559590cdbd5fdf9d435.png",
|
||||
"static/media/block.png": "/static/media/block.28912e0d5fb5aac2a32a.png",
|
||||
"static/media/logo.png": "/static/media/logo.c113459be1a6e40014fa.png",
|
||||
"index.html": "/index.html",
|
||||
"main.87caf152.css.map": "/static/css/main.87caf152.css.map",
|
||||
"main.63db921f.js.map": "/static/js/main.63db921f.js.map"
|
||||
"main.82b44b3f.css.map": "/static/css/main.82b44b3f.css.map",
|
||||
"main.1ed44755.js.map": "/static/js/main.1ed44755.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.87caf152.css",
|
||||
"static/js/main.63db921f.js"
|
||||
"static/css/main.82b44b3f.css",
|
||||
"static/js/main.1ed44755.js"
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 222 KiB |
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.63db921f.js"></script><link href="/static/css/main.87caf152.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>BlockXStarter</title><script defer="defer" src="/static/js/main.1ed44755.js"></script><link href="/static/css/main.82b44b3f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
|
@ -0,0 +1,98 @@
|
|||
/*! Moment Duration Format v2.2.2
|
||||
* https://github.com/jsmreese/moment-duration-format
|
||||
* Date: 2018-02-16
|
||||
*
|
||||
* Duration format plugin function for the Moment.js library
|
||||
* http://momentjs.com/
|
||||
*
|
||||
* Copyright 2018 John Madhavan-Reese
|
||||
* Released under the MIT license
|
||||
*/
|
||||
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.6.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.11.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.11.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* [js-sha3]{@link https://github.com/emn178/js-sha3}
|
||||
*
|
||||
* @version 0.8.0
|
||||
* @author Chen, Yi-Cyuan [emn178@gmail.com]
|
||||
* @copyright Chen, Yi-Cyuan 2015-2018
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
//! moment.js
|
||||
|
||||
//! moment.js locale configuration
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.6.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license React v0.20.2
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.2
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 244 KiB |
After Width: | Height: | Size: 268 KiB |
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"_format": "hh-sol-cache-2",
|
||||
"files": {
|
||||
"C:\\Users\\Achmad Sodiq\\Downloads\\flashdisk file faiz\\blockxstarter\\crowdfunding\\src\\contracts\\Crowdfunding.sol": {
|
||||
"lastModificationDate": 1718855249272,
|
||||
"contentHash": "e277a93f7addf6e03effa83c29c6f46e",
|
||||
"sourceName": "src/contracts/Crowdfunding.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.11",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.7"
|
||||
],
|
||||
"artifacts": [
|
||||
"Crowdfunding"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
require('@nomiclabs/hardhat-waffle')
|
||||
require('dotenv').config()
|
||||
|
||||
const { API_URL, PRIVATE_KEY } = process.env;
|
||||
|
||||
module.exports = {
|
||||
defaultNetwork: 'localhost',
|
||||
defaultNetwork: 'sepolia',
|
||||
networks: {
|
||||
localhost: {
|
||||
url: 'http://127.0.0.1:8545',
|
||||
sepolia: {
|
||||
url: API_URL,
|
||||
accounts: [`0x${PRIVATE_KEY}`]
|
||||
},
|
||||
},
|
||||
solidity: {
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
"eject": "react-scripts eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@nomiclabs/hardhat-ethers": "^2.1.0",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.3",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"ethereum-waffle": "^3.4.4",
|
||||
"ethers": "^5.6.9",
|
||||
"hardhat": "^2.10.1",
|
||||
|
@ -26,6 +26,8 @@
|
|||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "6",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-toastify": "^10.0.5",
|
||||
"react-typed": "^2.0.12",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
After Width: | Height: | Size: 222 KiB |
|
@ -24,7 +24,7 @@
|
|||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>BlockXStarter</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
const { ethers } = require('hardhat')
|
||||
const hre = require('hardhat')
|
||||
const fs = require('fs')
|
||||
|
||||
async function main() {
|
||||
const contract_name = ''
|
||||
const Contract = await ethers.getContractFactory(contract_name)
|
||||
const contract = await Contract.deploy()
|
||||
const taxFee = 5
|
||||
const Contract = await hre.ethers.getContractFactory('Crowdfunding')
|
||||
const contract = await Contract.deploy(taxFee)
|
||||
|
||||
await contract.deployed()
|
||||
|
||||
|
@ -21,4 +21,4 @@ async function main() {
|
|||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
})
|
42
src/App.jsx
|
@ -1,7 +1,45 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import Home from './views/Home'
|
||||
import Project from './views/Project'
|
||||
import { isWallectConnected } from './services/blockchain'
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
import Homepage from './views/Homepage'
|
||||
|
||||
const App = () => {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await isWallectConnected();
|
||||
console.log('Blockchain dimuat');
|
||||
setLoaded(true);
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<h4 className="text-3xl">Hello Hardhat and Tailwind</h4>
|
||||
<div className="min-h-screen relative">
|
||||
{loaded ? (
|
||||
<Routes>
|
||||
<Route path='/' element={<Homepage />} />
|
||||
<Route path='projects' element={<Home />} />
|
||||
<Route path='/projects/:id' element={<Project />} />
|
||||
</Routes>
|
||||
) : null}
|
||||
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"address": "0x6EDDfdEca0cacB915EA53AbD3a7D86757cd649CE"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "..\\..\\..\\build-info\\2dfcbeb6cd62675e9921eab4db6e9eaf.json"
|
||||
}
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 244 KiB |
After Width: | Height: | Size: 268 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.3 KiB |
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import gambar2 from '../assets/gambar2.png'
|
||||
|
||||
const Ablockchain = () => {
|
||||
return (
|
||||
<div className='w-full bg-white py-16 px-4'>
|
||||
<div className='max-w-[1248px] mx-auto grid md:grid-cols-2'>
|
||||
<img className='w-[500px] mx-auto my-4 p-5' src={gambar2} alt='/' style={{ borderRadius: '10%' }} />
|
||||
<div className='flex flex-col justify-center'>
|
||||
<p className='text-[#00cf79] font-bold'>SISTEM CROWDFUNDING BERTEKNOLOGI BLOCKCHAIN</p>
|
||||
<h1 className='md:text-4xl sm:text-3xl text-2xl font-bold py-2'>Apa Itu Blockchain?</h1>
|
||||
<p>
|
||||
Blockchain adalah teknologi penyimpanan data yang terdesentralisasi dan aman, terdiri dari blok-blok yang saling terhubung dan diamankan dengan kriptografi.
|
||||
Setiap blok mencatat transaksi dan terhubung dengan blok sebelumnya, sehingga sulit diubah tanpa konsensus mayoritas dalam jaringan. Teknologi ini transparan karena semua peserta dapat melihat semua transaksi,
|
||||
dan desentralisasi memastikan tidak ada otoritas tunggal yang mengontrol jaringan. Keamanan dijamin oleh teknik kriptografi, membuat data tahan terhadap manipulasi. Selain itu, blockchain memungkinkan penggunaan smart contracts untuk otomatisasi proses bisnis.
|
||||
Pertama dikenal melalui Bitcoin, blockchain memiliki aplikasi luas di berbagai industri, seperti keuangan, logistik, dan kesehatan.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Ablockchain
|
|
@ -0,0 +1,20 @@
|
|||
import { setGlobalState } from '../store'
|
||||
import { BsPlusLg } from 'react-icons/bs'
|
||||
|
||||
const AddButton = () => {
|
||||
return (
|
||||
<div className="fixed right-10 bottom-10 flex space-x-2 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center items-center w-9 h-9 bg-green-600
|
||||
text-white font-medium text-xs leading-tight uppercase
|
||||
rounded-full shadow-md hover:bg-green-700"
|
||||
onClick={() => setGlobalState('createModal', 'scale-100')}
|
||||
>
|
||||
<BsPlusLg className='font-bold' size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddButton
|
|
@ -0,0 +1,129 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { FaTimes, FaEthereum } from 'react-icons/fa';
|
||||
import { toast } from 'react-toastify';
|
||||
import { backProject } from '../services/blockchain';
|
||||
import { useGlobalState, setGlobalState } from '../store';
|
||||
import crowd from '../assets/crowd.png';
|
||||
|
||||
const BackProject = ({ project }) => {
|
||||
const [backModal] = useGlobalState('backModal');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [conversionRate, setConversionRate] = useState({ idr: null, usd: null });
|
||||
const [convertedAmount, setConvertedAmount] = useState('');
|
||||
const [selectedCurrency, setSelectedCurrency] = useState('idr');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConversionRate = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=idr,usd');
|
||||
const data = await response.json();
|
||||
setConversionRate({ idr: data.ethereum.idr, usd: data.ethereum.usd });
|
||||
} catch (error) {
|
||||
console.error('Tingkat konversi terdapat kesalahan:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConversionRate();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversionRate[selectedCurrency] && amount) {
|
||||
const converted = (parseFloat(amount) * conversionRate[selectedCurrency]).toLocaleString(selectedCurrency === 'idr' ? 'id-ID' : 'en-US', {
|
||||
style: 'currency',
|
||||
currency: selectedCurrency.toUpperCase()
|
||||
});
|
||||
setConvertedAmount(converted);
|
||||
} else {
|
||||
setConvertedAmount('');
|
||||
}
|
||||
}, [amount, conversionRate, selectedCurrency]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!amount) return;
|
||||
|
||||
await backProject(project?.id, amount);
|
||||
toast.success('Project telah berhasil didukung.');
|
||||
setGlobalState('backModal', 'scale-0');
|
||||
};
|
||||
|
||||
const handleClickOutside = (e) => {
|
||||
if (e.target.id === 'modal-overlay') {
|
||||
setGlobalState('backModal', 'scale-0');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="modal-overlay"
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex
|
||||
items-center justify-center bg-black bg-opacity-50
|
||||
transform transition-transform duration-300 ${backModal}`}
|
||||
onClick={handleClickOutside}
|
||||
>
|
||||
<div
|
||||
className="bg-white shadow-xl shadow-black
|
||||
rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="font-semibold">{project?.title}</p>
|
||||
<button
|
||||
onClick={() => setGlobalState('backModal', 'scale-0')}
|
||||
type="button"
|
||||
className="border-0 bg-transparent focus:outline-none"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center mt-5">
|
||||
<div className="rounded-xl overflow-hidden h-20 w-20">
|
||||
<img
|
||||
src={project?.imageURL || crowd}
|
||||
alt={project?.title}
|
||||
className="h-full w-full object-cover cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-gray-300 rounded-xl mt-5">
|
||||
<FaEthereum className="text-gray-500 text-xl mx-2" />
|
||||
<input
|
||||
className="block w-full bg-transparent border-0 text-sm text-slate-500 focus:outline-none focus:ring-0"
|
||||
type="number"
|
||||
step={0.0001}
|
||||
min={0.0001}
|
||||
name="amount"
|
||||
placeholder="Jumlah (ETH)"
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
value={amount}
|
||||
required
|
||||
/>
|
||||
<select
|
||||
className="block bg-transparent border-0 text-sm text-slate-500 focus:outline-none focus:ring-0"
|
||||
onChange={(e) => setSelectedCurrency(e.target.value)}
|
||||
value={selectedCurrency}
|
||||
>
|
||||
<option value="idr">IDR</option>
|
||||
<option value="usd">USD</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{convertedAmount && (
|
||||
<div className="mt-2 text-sm text-gray-500">{convertedAmount}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-md leading-tight rounded-full shadow-md hover:bg-green-700 mt-5"
|
||||
>
|
||||
Dukung Project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackProject;
|
|
@ -0,0 +1,252 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { FaTimes, FaEthereum } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import { createProject } from '../services/blockchain'
|
||||
import { useGlobalState, setGlobalState } from '../store'
|
||||
import crowd from '../assets/crowd.png'
|
||||
|
||||
const CreateProject = () => {
|
||||
const [createModal] = useGlobalState('createModal')
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [costETH, setCostETH] = useState('')
|
||||
const [costIDR, setCostIDR] = useState('')
|
||||
const [costUSD, setCostUSD] = useState('')
|
||||
const [formattedCostIDR, setFormattedCostIDR] = useState('')
|
||||
const [formattedCostUSD, setFormattedCostUSD] = useState('')
|
||||
const [date, setDate] = useState('')
|
||||
const [imageURL, setImageURL] = useState('')
|
||||
const [category, setCategory] = useState('')
|
||||
const [selectedCurrency, setSelectedCurrency] = useState('IDR')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchExchangeRate = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=idr,usd'
|
||||
)
|
||||
const data = await response.json()
|
||||
const ethToIdrRate = data.ethereum.idr
|
||||
const ethToUsdRate = data.ethereum.usd
|
||||
if (ethToIdrRate && costETH) {
|
||||
const costInIDR = costETH * ethToIdrRate
|
||||
setCostIDR(costInIDR.toFixed(2))
|
||||
setFormattedCostIDR(formatNumberWithCommas(costInIDR.toFixed(2)))
|
||||
}
|
||||
if (ethToUsdRate && costETH) {
|
||||
const costInUSD = costETH * ethToUsdRate
|
||||
setCostUSD(costInUSD.toFixed(2))
|
||||
setFormattedCostUSD(formatNumberWithCommas(costInUSD.toFixed(2)))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Tingkat konversi terdapat kesalahan:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchExchangeRate()
|
||||
}, [costETH])
|
||||
|
||||
const formatNumberWithCommas = (number) => {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
}
|
||||
|
||||
const toTimestamp = (dateStr) => {
|
||||
const dateObj = Date.parse(dateStr)
|
||||
return dateObj / 1000
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!title || !description || !costETH || !date || !imageURL || !category) return
|
||||
|
||||
const params = {
|
||||
title,
|
||||
description,
|
||||
cost: costETH,
|
||||
expiresAt: toTimestamp(date),
|
||||
imageURL,
|
||||
category,
|
||||
}
|
||||
|
||||
await createProject(params)
|
||||
toast.success('Project telah berhasil dibuat.')
|
||||
onClose()
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setGlobalState('createModal', 'scale-0')
|
||||
reset()
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
setTitle('')
|
||||
setCostETH('')
|
||||
setCostIDR('')
|
||||
setCostUSD('')
|
||||
setFormattedCostIDR('')
|
||||
setFormattedCostUSD('')
|
||||
setDescription('')
|
||||
setImageURL('')
|
||||
setDate('')
|
||||
setCategory('')
|
||||
}
|
||||
|
||||
const handleOverlayClick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCurrency = () => {
|
||||
setSelectedCurrency(selectedCurrency === 'IDR' ? 'USD' : 'IDR')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${createModal}`}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div
|
||||
className="bg-white shadow-xl rounded-xl w-11/12 md:w-2/5 p-8"
|
||||
style={{ maxHeight: '80vh', overflowY: 'auto' }}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<p className="text-2xl font-semibold">Tambah Project</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors duration-300"
|
||||
>
|
||||
<FaTimes size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center mb-6">
|
||||
<div className="rounded-xl overflow-hidden h-20 w-20">
|
||||
<img
|
||||
src={imageURL || crowd}
|
||||
alt="project title"
|
||||
className="h-full w-full object-cover cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
required
|
||||
>
|
||||
<option value="">Pilih Kategori</option>
|
||||
<option value="Teknologi Informasi">Teknologi Informasi</option>
|
||||
<option value="Keuangan">Keuangan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Seni dan Budaya">Seni dan Budaya</option>
|
||||
<option value="Lingkungan">Lingkungan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="Judul"
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
value={title}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center bg-gray-100 border border-gray-300 rounded-lg">
|
||||
<FaEthereum className="text-gray-500 ml-3" />
|
||||
<input
|
||||
className="block w-full bg-transparent border-0 text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
type="number"
|
||||
step={0.0001}
|
||||
min={0.0001}
|
||||
name="costETH"
|
||||
placeholder="Biaya (ETH)"
|
||||
onChange={(e) => setCostETH(e.target.value)}
|
||||
value={costETH}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center bg-gray-100 border border-gray-300 rounded-lg">
|
||||
<span className="text-gray-500 ml-3">{selectedCurrency === 'IDR' ? 'Rp' : '$'}</span>
|
||||
<input
|
||||
className="block w-full bg-transparent border-0 text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
type="text"
|
||||
name="costCurrency"
|
||||
placeholder={`Biaya (${selectedCurrency})`}
|
||||
value={selectedCurrency === 'IDR' ? formattedCostIDR : formattedCostUSD}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-4 text-sm text-blue-500 px-2"
|
||||
onClick={toggleCurrency}
|
||||
>
|
||||
{selectedCurrency === 'IDR' ? 'Tampilkan USD' : 'Tampilkan IDR'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
type="date"
|
||||
name="date"
|
||||
placeholder="Tanggal Berakhir"
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
value={date}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg text-gray-700 py-2 px-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
type="url"
|
||||
name="imageURL"
|
||||
placeholder="URL Gambar"
|
||||
onChange={(e) => setImageURL(e.target.value)}
|
||||
value={imageURL}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<textarea
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg text-gray-700 py-2 px-3 leading-tight focus:outline-none focus
|
||||
focus:bg-white focus:border-gray-500"
|
||||
type="text"
|
||||
name="description"
|
||||
placeholder="Deskripsi"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
value={description}
|
||||
required
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-block w-full px-6 py-3 bg-green-600 text-white font-medium text-lg leading-tight rounded-lg shadow-md hover:bg-green-700 transition duration-300"
|
||||
>
|
||||
Buat Project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateProject
|
|
@ -0,0 +1,72 @@
|
|||
import { FaTimes } from 'react-icons/fa'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { deleteProject } from '../services/blockchain'
|
||||
import { useGlobalState, setGlobalState } from '../store'
|
||||
import crowd from '../assets/crowd.png'
|
||||
import ProjectDetails from './ProjectDetails'
|
||||
|
||||
const DeleteProject = ({ project }) => {
|
||||
const [deleteModal] = useGlobalState('deleteModal')
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await deleteProject(project?.id)
|
||||
toast.success('Project telah berhasil dihapus.')
|
||||
setGlobalState('deleteModal', 'scale-0')
|
||||
navigate(ProjectDetails)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setGlobalState('deleteModal', 'scale-0')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${deleteModal}`}
|
||||
onClick={closeModal}
|
||||
>
|
||||
<div
|
||||
className="bg-white shadow-xl rounded-xl w-11/12 md:w-2/5 h-auto p-6 transform transition-transform duration-300"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between items-center border-b pb-3 mb-4">
|
||||
<p className="text-lg font-semibold text-gray-800">{project?.title}</p>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border-0 bg-transparent text-gray-600 hover:text-gray-900 focus:outline-none"
|
||||
>
|
||||
<FaTimes size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center mt-5">
|
||||
<div className="rounded-xl overflow-hidden h-20 w-20 border">
|
||||
<img
|
||||
src={project?.imageURL || crowd}
|
||||
alt={project?.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center items-center text-center mt-5">
|
||||
<p className="text-lg text-gray-700">Anda yakin ingin menghapus proyek ini?</p>
|
||||
<small className="text-red-500 mt-1">Tindakan ini tidak dapat diurungkan.</small>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-md leading-tight rounded-full shadow-md hover:bg-red-700 focus:outline-none transition duration-150 ease-in-out mt-5"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Hapus Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteProject
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react'
|
||||
import block from '../assets/block.png'
|
||||
import {
|
||||
FaFacebookSquare,
|
||||
FaInstagram,
|
||||
FaTwitterSquare,
|
||||
} from 'react-icons/fa'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<div className='max-w-[1940px] mx-auto bg-black py-16 px-4 grid lg:grid-cols-3 gap-8 text-gray-300'>
|
||||
<div>
|
||||
<img src={block} alt="/" style={{ width: '250px', height: 'auto' }} />
|
||||
<p className='py-4'>Selamat datang di BlockXStarter! Platform antara para pencari dana dan proyek-proyek berbasis blockchain.</p>
|
||||
<div className='flex justify-between md:w-[75%] my-6'>
|
||||
<FaFacebookSquare size={30} />
|
||||
<FaInstagram size={30} />
|
||||
<FaTwitterSquare size={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='lg:col-span-2 flex justify-between mt-6'>
|
||||
<div>
|
||||
<h6 className='font-medium text-gray-400'>Kontak Kami</h6>
|
||||
<ul>
|
||||
<li className='py-2 text-sm' >Email: support@blockxstarter.blockchain</li>
|
||||
<li className='py-2 text-sm'>Telepon: +62 896 5624 5757</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className='font-medium text-gray-400'>Tautan Cepat</h6>
|
||||
<ul>
|
||||
<li className='py-2 text-sm'>Beranda</li>
|
||||
<li className='py-2 text-sm'>Proyek</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className='font-medium text-gray-400'>Legal</h6>
|
||||
<ul>
|
||||
<li className='py-2 text-sm'>Kebijakan Privasi</li>
|
||||
<li className='py-2 text-sm'>Syarat & Ketentuan</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
|
@ -0,0 +1,82 @@
|
|||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { connectWallet } from '../services/blockchain';
|
||||
import { truncate, useGlobalState, setGlobalState } from '../store';
|
||||
import { FaBars, FaHome, FaPlus, FaProjectDiagram } from 'react-icons/fa';
|
||||
|
||||
import logo from '../assets/logo.png';
|
||||
|
||||
const Header = () => {
|
||||
const [connectedAccount] = useGlobalState('connectedAccount');
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setShowDropdown(!showDropdown);
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setGlobalState('createModal', 'scale-100');
|
||||
navigate('/projects');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className='flex justify-between items-center h-24 max-w-[1940px] mx-auto px-4 bg-black'>
|
||||
<Link to='/'>
|
||||
<img src={logo} alt='/' style={{ width: '200px', height: 'auto' }} />
|
||||
</Link>
|
||||
|
||||
<div className='flex space-x-4 justify-center relative'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex items-center px-4 py-2 text-white bg-[#2e3033] border border-[#2e3033] rounded-md transition duration-300 hover:bg-[#0e0f10] hover:border-[#0e0f10]'
|
||||
onClick={toggleDropdown}
|
||||
>
|
||||
<FaBars />
|
||||
<span className="ml-2">Menu</span>
|
||||
</button>
|
||||
|
||||
{showDropdown && (
|
||||
<div className='absolute z-10 top-full left-0 w-48 bg-white border rounded shadow-md' onMouseLeave={closeDropdown}>
|
||||
<Link to='/' className='block px-4 py-2 text-gray-800 hover:bg-gray-200'>
|
||||
<FaHome className='inline-block mr-2' /> Halaman Utama
|
||||
</Link>
|
||||
<Link to='/projects' className='block px-4 py-2 text-gray-800 hover:bg-gray-200'>
|
||||
<FaProjectDiagram className='inline-block mr-2' /> Project
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="block px-4 py-2 text-gray-800 hover:bg-gray-200 w-full text-left"
|
||||
onClick={openCreateModal}
|
||||
>
|
||||
<FaPlus className='inline-block mr-2' /> Tambah Project
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectedAccount ? (
|
||||
<button
|
||||
type='button'
|
||||
className='inline-block px-6 py-2.5 bg-[#8c6dfd] text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-[#5E1675]'
|
||||
>
|
||||
{truncate(connectedAccount, 4, 4, 11)}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type='button'
|
||||
className='inline-block px-6 py-2.5 bg-[#8c6dfd] text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-[#5E1675]'
|
||||
onClick={connectWallet}
|
||||
>
|
||||
Hubungkan Wallet
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
|
@ -0,0 +1,44 @@
|
|||
import { useGlobalState } from '../store'
|
||||
import { FaProjectDiagram, FaHandsHelping, FaDonate } from 'react-icons/fa'
|
||||
|
||||
const Hero = () => {
|
||||
const [stats] = useGlobalState('stats')
|
||||
|
||||
return (
|
||||
<div className="text-center bg-black text-white py-20 px-6">
|
||||
<h1
|
||||
className="text-5xl md:text-6xl xl:text-7xl font-bold tracking-tight mb-12"
|
||||
>
|
||||
<span className="capitalize">Wujudkan Project Bersama</span>
|
||||
<br />
|
||||
<span className="uppercase text-green-600">blockxstarter</span>
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-10">
|
||||
<div className="flex flex-col justify-center items-center h-32 bg-gray-800 rounded-lg shadow-lg p-4">
|
||||
<FaProjectDiagram className="text-3xl text-[#3ABEF9] mb-2" />
|
||||
<span className="text-2xl font-bold text-[#3ABEF9] leading-5">
|
||||
{stats?.totalProjects || 0}
|
||||
</span>
|
||||
<span className="text-lg">Total Project</span>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center h-32 bg-gray-800 rounded-lg shadow-lg p-4">
|
||||
<FaHandsHelping className="text-3xl text-[#3ABEF9] mb-2" />
|
||||
<span className="text-2xl font-bold text-[#3ABEF9] leading-5">
|
||||
{stats?.totalBacking || 0}
|
||||
</span>
|
||||
<span className="text-lg">Total Dukungan</span>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center h-32 bg-gray-800 rounded-lg shadow-lg p-4">
|
||||
<FaDonate className="text-3xl text-[#3ABEF9] mb-2" />
|
||||
<span className="text-2xl font-bold text-[#3ABEF9] leading-5">
|
||||
{stats?.totalDonations || 0} ETH
|
||||
</span>
|
||||
<span className="text-lg">Total Donasi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import logo from '../assets/logo.png';
|
||||
import { AiOutlineClose, AiOutlineMenu } from 'react-icons/ai';
|
||||
|
||||
const Navbar = () => {
|
||||
const [nav, setNav] = useState(false);
|
||||
|
||||
const handleNav = () => {
|
||||
setNav(!nav);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='sticky top-0 z-50 flex justify-between items-center h-24 w-full max-w-[1940px] mx-auto px-4 bg-[#000300] text-white'>
|
||||
<img src={logo} alt="Logo" style={{ width: '200px', height: 'auto' }} />
|
||||
<ul className='hidden md:flex'>
|
||||
<li className='p-4'>
|
||||
<Link to='/'>
|
||||
Halaman Utama
|
||||
</Link>
|
||||
</li>
|
||||
<li className='p-4'>
|
||||
<Link to='/projects'>
|
||||
Semua Projects
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div onClick={handleNav} className='block md:hidden'>
|
||||
{nav ? <AiOutlineClose size={20} /> : <AiOutlineMenu size={20} />}
|
||||
</div>
|
||||
<div className={nav ? 'fixed left-0 top-0 w-[60%] h-full border-r border-r-gray-900 bg-[#000300] ease-in-out duration-500' : 'fixed left-[-100%] ease-in-out duration-500'}>
|
||||
<img src={logo} alt="Logo" style={{ width: '200px', height: 'auto' }} className='p-4' />
|
||||
<ul className='uppercase p-4'>
|
||||
<li className='p-4 border-b border-gray-600'>
|
||||
<Link to='/' onClick={handleNav}>
|
||||
Halaman Utama
|
||||
</Link>
|
||||
</li>
|
||||
<li className='p-4 border-b border-gray-600'>
|
||||
<Link to='/projects' onClick={handleNav}>
|
||||
Semua Projects
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
const Newsletter = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleEmailChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handleEmailClick = () => {
|
||||
if (!email || !email.includes('@')) {
|
||||
alert('Mohon masukkan alamat email yang valid.');
|
||||
return;
|
||||
}
|
||||
window.location.href = `https://mail.google.com/mail/?view=cm&fs=1&to=developerfaiz@gmail.com`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full py-16 text-white px-4 bg-black'>
|
||||
<div className='max-w-[1240px] mx-auto grid lg:grid-cols-3'>
|
||||
<div className='lg:col-span-2 my-4'>
|
||||
<h1 className='md:text-4xl sm:text-3xl text-2xl font-bold py-2'>Ingin informasi lebih detail?</h1>
|
||||
<p>Dapat menghubungi via email.</p>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-between w-full'>
|
||||
<input className='p-3 flex w-full rounded-md text-black' type='email' placeholder='Masukkan Email' value={email} onChange={handleEmailChange} />
|
||||
<button className='bg-[#00df9a] text-black rounded-md font-medium w-[200px] ml-4 my-6 px-6 py-3' onClick={handleEmailClick}>Kirim Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Newsletter;
|
|
@ -0,0 +1,58 @@
|
|||
import { FaEthereum, FaArrowRight } from 'react-icons/fa';
|
||||
import Identicon from 'react-identicons';
|
||||
import Moment from 'react-moment';
|
||||
import 'moment/locale/id';
|
||||
import { truncate } from '../store';
|
||||
|
||||
const ProjectBackers = ({ backers }) => {
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-start w-full px-6 mx-auto">
|
||||
<div className="overflow-hidden shadow-md rounded-md w-full mb-10">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dukungan</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Donasi</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pengembalian</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Waktu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{backers.map((backer, i) => (
|
||||
<Backer key={i} backer={backer} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Backer = ({ backer }) => (
|
||||
<tr className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<Identicon className="h-8 w-8 rounded-full" string={backer.owner} size={25} />
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">{truncate(backer.owner, 4, 4, 11)}</div>
|
||||
<a href={`https://sepolia.etherscan.io/address/${backer.owner}`} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-500 hover:underline flex items-center mt-1">
|
||||
<span>Lihat di Etherscan</span>
|
||||
<FaArrowRight className="ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<FaEthereum className="h-4 w-4 text-gray-400" />
|
||||
<div className="ml-2 text-sm text-gray-900">{backer.contribution} ETH</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={backer.refunded ? "px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" : "px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"}>{backer.refunded ? 'Ya' : 'Tidak'}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><Moment locale="id" fromNow>{backer.timestamp}</Moment></td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
export default ProjectBackers;
|
|
@ -0,0 +1,243 @@
|
|||
import Identicons from 'react-identicons';
|
||||
import { FaEthereum, FaExclamationCircle } from 'react-icons/fa';
|
||||
import {
|
||||
daysRemaining,
|
||||
setGlobalState,
|
||||
truncate,
|
||||
useGlobalState,
|
||||
} from '../store';
|
||||
import { payoutProject, addComment } from '../services/blockchain';
|
||||
import { useState } from 'react';
|
||||
|
||||
const ProjectDetails = ({ project }) => {
|
||||
const [connectedAccount] = useGlobalState('connectedAccount');
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const expired = new Date().getTime() > Number(project?.expiresAt + '000');
|
||||
|
||||
const handleAddComment = async () => {
|
||||
if (newComment.trim() === '') {
|
||||
alert('Komentar tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
await addComment(project.id, newComment);
|
||||
setNewComment('');
|
||||
setGlobalState('project', { ...project }); // Refresh project to get new comments
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-16 mb-5 px-6 flex justify-center bg-gray-100">
|
||||
<div className="w-full max-w-screen-xl bg-white shadow-lg rounded-lg p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2">
|
||||
<img
|
||||
src={project?.imageURL}
|
||||
alt={project?.title}
|
||||
className="rounded-xl w-full mb-4 object-contain"
|
||||
style={{ maxHeight: '400px' }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col mt-4">
|
||||
<h5 className="text-gray-900 text-3xl font-semibold mb-2">
|
||||
{project?.title}
|
||||
</h5>
|
||||
<small className={`text-gray-500 ${expired ? 'text-red-500' : ''}`}>
|
||||
{expired
|
||||
? 'Berakhir'
|
||||
: daysRemaining(project.expiresAt) + ' tersisa'}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center w-full mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Identicons
|
||||
className="rounded-full shadow-md"
|
||||
string={project?.owner}
|
||||
size={30}
|
||||
/>
|
||||
{project?.owner ? (
|
||||
<small className="text-gray-700 font-medium">
|
||||
{truncate(project?.owner)}
|
||||
</small>
|
||||
) : null}
|
||||
<small className="text-gray-500 font-bold">
|
||||
{project?.backers} Dukungan
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="font-bold text-sm">
|
||||
{expired ? (
|
||||
<small className="text-red-500">Berakhir</small>
|
||||
) : project?.status === 0 ? (
|
||||
<small className="text-blue-500">Terbuka</small>
|
||||
) : project?.status === 1 ? (
|
||||
<small className="text-green-500">Diterima</small>
|
||||
) : project?.status === 2 ? (
|
||||
<small className="text-orange-500">Dikembalikan</small>
|
||||
) : project?.status === 3 ? (
|
||||
<small className="text-gray-600">Dihapus</small>
|
||||
) : (
|
||||
<small className="text-purple-500">Dibayar</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-base text-gray-700 font-light mt-4">{project?.description}</p>
|
||||
|
||||
<div className="w-full overflow-hidden bg-gray-300 rounded-full mt-4">
|
||||
<div
|
||||
className="bg-green-600 text-xs font-medium text-green-100 text-center p-0.5 leading-none rounded-full"
|
||||
style={{
|
||||
width: `${(project?.raised / project?.cost) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center font-bold mt-2">
|
||||
<small>{project?.raised} ETH Terkumpul</small>
|
||||
<small className="flex items-center">
|
||||
<FaEthereum />
|
||||
<span className="ml-1">{project?.cost} ETH</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-start items-center space-x-2 mt-4">
|
||||
{expired ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-gray-400 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md cursor-not-allowed"
|
||||
>
|
||||
Project Ditutup
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
{connectedAccount !== project?.owner && project?.status === 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-green-700"
|
||||
onClick={() => setGlobalState('backModal', 'scale-100')}
|
||||
>
|
||||
Dukung Project
|
||||
</button>
|
||||
)}
|
||||
|
||||
{connectedAccount === project?.owner && project?.status !== 3 && (
|
||||
<>
|
||||
{project?.status === 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-orange-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-orange-700"
|
||||
onClick={() => payoutProject(project?.id)}
|
||||
>
|
||||
Cairkan Dana
|
||||
</button>
|
||||
) : project?.status !== 4 ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-gray-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-gray-700"
|
||||
onClick={() => setGlobalState('updateModal', 'scale-100')}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-red-700"
|
||||
onClick={() => setGlobalState('deleteModal', 'scale-100')}
|
||||
>
|
||||
Hapus
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-block px-6 py-2.5 bg-gray-400 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md cursor-not-allowed"
|
||||
>
|
||||
Project Ditutup
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<small className="text-gray-500 font-semibold uppercase tracking-wide">
|
||||
Kategori:
|
||||
<span className="ml-1 text-gray-900 font-bold">{project?.category}</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comments Section */}
|
||||
<div className="md:col-span-1 mt-6 md:mt-0">
|
||||
<h3 className="text-lg font-semibold mb-4">Dukungan Komentar</h3>
|
||||
<div className="space-y-4 h-96 overflow-y-auto">
|
||||
{project?.status !== 3 && project?.status !== 4 && (
|
||||
project?.comments?.length > 0 ? (
|
||||
project?.comments.map((comment, index) => (
|
||||
<div key={index} className="flex items-start space-x-4">
|
||||
<Identicons
|
||||
className="rounded-full shadow-md"
|
||||
string={comment.commenter}
|
||||
size={30}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className={`p-4 rounded-lg ${comment.commenter === project.owner ? 'bg-blue-100' : 'bg-gray-100'}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{truncate(comment.commenter, 4, 4, 11)}
|
||||
{comment.commenter === project.owner && (
|
||||
<span className="ml-2 text-xs text-blue-600 font-bold">(Pemilik)</span>
|
||||
)}
|
||||
</h4>
|
||||
<small className="text-gray-500">{new Date(comment.timestamp).toLocaleString()}</small>
|
||||
</div>
|
||||
<p className="text-gray-700 mt-2">{comment.comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-gray-500">Belum ada komentar untuk project ini.</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{(project?.status === 3 || project?.status === 4) && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-gray-500">Komentar tidak tersedia karena project telah {project?.status === 3 ? 'dihapus' : 'dibayar'}.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
{(project?.status !== 3 && project?.status !== 4 && connectedAccount && !expired) && (
|
||||
<>
|
||||
<textarea
|
||||
className="w-full p-4 bg-gray-100 rounded-lg"
|
||||
rows="4"
|
||||
placeholder="Tambahkan komentar"
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center mt-2">
|
||||
<button
|
||||
className="px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700"
|
||||
onClick={handleAddComment}
|
||||
>
|
||||
Tambah Komentar
|
||||
</button>
|
||||
<FaExclamationCircle className="ml-2 text-grey-500 cursor-pointer" title="Hanya pengguna yang telah mendukung project ini yang dapat menambahkan komentar" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectDetails;
|
|
@ -0,0 +1,146 @@
|
|||
import Identicons from 'react-identicons';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FaEthereum } from 'react-icons/fa';
|
||||
import { truncate, daysRemaining } from '../store';
|
||||
|
||||
const Projects = ({ projects }) => {
|
||||
const [end, setEnd] = useState(4);
|
||||
const [count] = useState(4);
|
||||
const [collection, setCollection] = useState([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||||
|
||||
const getCollection = () => {
|
||||
if (!selectedCategory) {
|
||||
return projects.slice(0, end);
|
||||
} else {
|
||||
return projects.filter(project => project.category === selectedCategory).slice(0, end);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryClick = (category) => {
|
||||
setSelectedCategory(category);
|
||||
setEnd(4);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(getCollection());
|
||||
}, [projects, end, selectedCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
||||
if (projects.length > collection.length) {
|
||||
setEnd(end + count);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [end, collection, projects, count]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-6 mb-7">
|
||||
<div className="flex justify-center gap-4 mb-5 py-3 flex-wrap">
|
||||
{['Semua', 'Teknologi Informasi', 'Keuangan', 'Kesehatan', 'Pendidikan', 'Seni dan Budaya', 'Lingkungan'].map((category, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`text-sm font-medium py-1.5 px-4 rounded-full shadow-md transition-transform duration-300 ${
|
||||
selectedCategory === (category === 'Semua' ? null : category)
|
||||
? 'bg-blue-600 text-white transform scale-105'
|
||||
: 'bg-white text-blue-600 border border-blue-600 hover:bg-blue-100'
|
||||
}`}
|
||||
onClick={() => handleCategoryClick(category === 'Semua' ? null : category)}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{collection.map((project, i) => (
|
||||
<ProjectCard key={i} project={project} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectCard = ({ project }) => {
|
||||
const expired = new Date().getTime() > Number(project?.expiresAt + '000');
|
||||
|
||||
return (
|
||||
<div id="projects" className="rounded-lg shadow-lg bg-white hover:shadow-xl transition duration-300">
|
||||
<Link to={'/projects/' + project.id} className="block">
|
||||
<div className="overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={project.imageURL}
|
||||
alt={project.title}
|
||||
className="h-64 w-full object-cover transform hover:scale-105 transition duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<h5 className="text-xl font-semibold mb-2">{truncate(project.title, 25, 0, 28)}</h5>
|
||||
|
||||
<div className="flex items-center mb-3">
|
||||
<Identicons className="rounded-full shadow-md" string={project.owner} size={15} />
|
||||
<small className="text-gray-700 ml-2">{truncate(project.owner, 4, 4, 11)}</small>
|
||||
</div>
|
||||
|
||||
<small className="text-gray-500 block mb-3">
|
||||
{expired ? 'Berakhir' : `${daysRemaining(project.expiresAt)} tersisa`}
|
||||
</small>
|
||||
|
||||
<div className="w-full bg-gray-200 h-2 rounded-full overflow-hidden mb-3">
|
||||
<div
|
||||
className="bg-green-600 h-full text-xs font-medium text-green-100 text-center leading-none"
|
||||
style={{ width: `${(project.raised / project.cost) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div>
|
||||
<small className="text-sm font-bold">
|
||||
{project.raised} ETH Terkumpul ({((project.raised / project.cost) * 100).toFixed(2)}%)
|
||||
</small>
|
||||
<small className="text-xs flex items-center text-gray-500 mt-1">
|
||||
<FaEthereum className="mr-1" />
|
||||
<span>{project.cost} ETH</span>
|
||||
</small>
|
||||
</div>
|
||||
<small
|
||||
className={`text-sm font-bold ${
|
||||
expired ? 'text-red-500' :
|
||||
project.status === 0 ? 'text-blue-500' :
|
||||
project.status === 1 ? 'text-green-500' :
|
||||
project.status === 2 ? 'text-orange-500' :
|
||||
project.status === 3 ? 'text-gray-600' :
|
||||
'text-purple-500'
|
||||
}`}
|
||||
>
|
||||
{expired ? 'Berakhir' :
|
||||
project.status === 0 ? 'Terbuka' :
|
||||
project.status === 1 ? 'Diterima' :
|
||||
project.status === 2 ? 'Dikembalikan' :
|
||||
project.status === 3 ? 'Dihapus' :
|
||||
'Dibayar'
|
||||
}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center text-sm font-bold">
|
||||
<small>{project.backers} Dukungan{project.backers === 1 ? '' : ''}</small>
|
||||
<small className="text-gray-500">
|
||||
<span className="bg-gray-200 text-gray-700 rounded-full px-2 py-1 text-xs font-semibold">
|
||||
{project.category}
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ReactTyped } from 'react-typed';
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<div className='text-white bg-black'>
|
||||
<div className='max-w-[800px] mt-[-48px] w-full h-screen mx-auto text-center flex flex-col justify-center'>
|
||||
<p className='text-[#00cf79] font-bold p-2'>BlockXStarter</p>
|
||||
<h1 className='md:text-7xl sm:text-6xl text-4xl font-bold md:py-6'>Wujudkan Project Anda Menjadi Nyata</h1>
|
||||
<div className='flex justify-center items-center'>
|
||||
<p className='md:text-5xl sm:text-4xl text-xl font-bold py-4'>Sistem</p>
|
||||
<ReactTyped
|
||||
className='md:text-5xl sm:text-4xl text-xl font-bold md:pl-4 pl-2'
|
||||
strings={['KONSENSUS', 'IMMUTABLE', 'TRANSPARAN', 'DESENTRALISASI']}
|
||||
typeSpeed={120}
|
||||
backSpeed={140}
|
||||
loop
|
||||
/>
|
||||
</div>
|
||||
<button className='bg-[#8c6dfd] w-[200px] rounded-md font-medium my-6 mx-auto py-3 text-black hover:bg-[#5E1675]'>
|
||||
<Link to='/projects'>
|
||||
Mulai Sekarang ➤
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Title;
|
|
@ -0,0 +1,162 @@
|
|||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { toast } from 'react-toastify'
|
||||
import { updateProject } from '../services/blockchain'
|
||||
import { useGlobalState, setGlobalState } from '../store'
|
||||
import crowd from '../assets/crowd.png'
|
||||
|
||||
const UpdateProject = ({ project }) => {
|
||||
const [updateModal] = useGlobalState('updateModal')
|
||||
const [title, setTitle] = useState(project?.title)
|
||||
const [description, setDescription] = useState(project?.description)
|
||||
const [date, setDate] = useState(project?.date)
|
||||
const [imageURL, setImageURL] = useState(project?.imageURL)
|
||||
const [category] = useState(project?.category)
|
||||
const modalRef = useRef(null)
|
||||
|
||||
const toTimestamp = (dateStr) => {
|
||||
const dateObj = Date.parse(dateStr)
|
||||
return dateObj / 1000
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!title || !description || !date || !imageURL || !category) return
|
||||
|
||||
const params = {
|
||||
id: project?.id,
|
||||
title,
|
||||
description,
|
||||
expiresAt: toTimestamp(date),
|
||||
imageURL,
|
||||
category,
|
||||
}
|
||||
|
||||
await updateProject(params)
|
||||
toast.success('Project telah berhasil diupdate.')
|
||||
onClose()
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setGlobalState('updateModal', 'scale-0')
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex
|
||||
items-center justify-center bg-black bg-opacity-50
|
||||
transform transition-transform duration-300 ${updateModal}`}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="bg-white shadow-xl shadow-black
|
||||
rounded-xl w-11/12 md:w-2/5 p-8 transform transition-transform duration-500"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col space-y-5">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-700">Edit Project</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
<FaTimes size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="rounded-xl overflow-hidden h-20 w-20">
|
||||
<img
|
||||
src={imageURL || crowd}
|
||||
alt="project title"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg px-4 py-2 text-gray-700 focus:outline-none focus:border-blue-500"
|
||||
type="text"
|
||||
name="title"
|
||||
placeholder="Judul"
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
value={title}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg px-4 py-2 text-gray-700 focus:outline-none focus:border-blue-500"
|
||||
type="date"
|
||||
name="date"
|
||||
placeholder="Expires"
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
value={date}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg px-4 py-2 text-gray-700 focus:outline-none focus:border-blue-500"
|
||||
type="url"
|
||||
name="imageURL"
|
||||
placeholder="URL Gambar"
|
||||
onChange={(e) => setImageURL(e.target.value)}
|
||||
value={imageURL}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg px-4 py-2 text-gray-700 focus:outline-none focus:border-blue-500"
|
||||
type="text"
|
||||
name="category"
|
||||
placeholder="Kategori"
|
||||
value={category}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
className="block w-full bg-gray-100 border border-gray-300 rounded-lg px-4 py-2 text-gray-700 focus:outline-none focus:border-blue-500"
|
||||
name="description"
|
||||
placeholder="Deskripsi"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
value={description}
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-6 py-2.5 bg-blue-600
|
||||
text-white font-medium text-md leading-tight
|
||||
rounded-lg shadow-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateProject
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import gambar1 from '../assets/gambar1.png'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const Why = () => {
|
||||
return (
|
||||
<div className='w-full bg-white py-16 px-4'>
|
||||
<div className='max-w-[1248px] mx-auto grid md:grid-cols-2'>
|
||||
<img className='w-[500px] mx-auto my-4 p-5' src={gambar1} alt='/' />
|
||||
<div className='flex flex-col justify-center'>
|
||||
<p className='text-[#00cf79] font-bold'>SISTEM CROWDFUNDING BERTEKNOLOGI BLOCKCHAIN</p>
|
||||
<h1 className='md:text-4xl sm:text-3xl text-2xl font-bold py-2'>Kelebihan Crowdfunding Blockchain?</h1>
|
||||
<p>
|
||||
Blocxstarter berbasis crowdfunding blockchain menawarkan transparansi tinggi, keamanan kuat,
|
||||
dan menghilangkan perantara atau pihak ketiga. Jaringan terdesentralisasi ini memungkinkan akses global,
|
||||
memberikan peluang bagi pengguna untuk menjangkau investor di seluruh dunia. Dengan smart contracts, dana dikelola otomatis berdasarkan kesepakatan yang telah ditetapkan,
|
||||
memberikan perlindungan tambahan bagi investor. Selain itu, token yang diterbitkan dapat diperdagangkan, memberikan likuiditas dan model partisipasi yang lebih demokratis.
|
||||
Secara keseluruhan, blockchain membawa inovasi dan efisiensi baru ke dalam model crowdfunding, menjadikannya pilihan yang lebih menarik dan terpercaya.
|
||||
</p>
|
||||
<button className='bg-black text-[#8c6dfd] w-[200px] rounded-md font-medium my-6 mx-auto md:mx-0 py-3'>
|
||||
<Link to='/projects'>
|
||||
Mulai Sekarang
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Why
|
|
@ -0,0 +1,333 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.7;
|
||||
|
||||
contract Crowdfunding {
|
||||
address public owner;
|
||||
uint public projectTax;
|
||||
uint public projectCount;
|
||||
uint public balance;
|
||||
statsStruct public stats;
|
||||
projectStruct[] public projects;
|
||||
|
||||
mapping(address => projectStruct[]) projectsOf;
|
||||
mapping(uint => backerStruct[]) backersOf;
|
||||
mapping(uint => commentStruct[]) commentsOf;
|
||||
mapping(uint => bool) public projectExist;
|
||||
mapping(string => uint[]) categoryProjects;
|
||||
|
||||
enum statusEnum {
|
||||
OPEN,
|
||||
APPROVED,
|
||||
REVERTED,
|
||||
DELETED,
|
||||
PAIDOUT
|
||||
}
|
||||
|
||||
struct statsStruct {
|
||||
uint totalProjects;
|
||||
uint totalBacking;
|
||||
uint totalDonations;
|
||||
}
|
||||
|
||||
struct backerStruct {
|
||||
address owner;
|
||||
uint contribution;
|
||||
uint timestamp;
|
||||
bool refunded;
|
||||
}
|
||||
|
||||
struct commentStruct {
|
||||
address commenter;
|
||||
string comment;
|
||||
uint timestamp;
|
||||
}
|
||||
|
||||
struct projectStruct {
|
||||
uint id;
|
||||
address owner;
|
||||
string title;
|
||||
string description;
|
||||
string imageURL;
|
||||
string category;
|
||||
uint cost;
|
||||
uint raised;
|
||||
uint timestamp;
|
||||
uint expiresAt;
|
||||
uint backers;
|
||||
statusEnum status;
|
||||
}
|
||||
|
||||
modifier ownerOnly() {
|
||||
require(msg.sender == owner, "Hanya untuk pemilik");
|
||||
_;
|
||||
}
|
||||
|
||||
event Action (
|
||||
uint256 id,
|
||||
string actionType,
|
||||
address indexed executor,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
constructor(uint _projectTax) {
|
||||
owner = msg.sender;
|
||||
projectTax = _projectTax;
|
||||
}
|
||||
|
||||
function createProject(
|
||||
string memory title,
|
||||
string memory description,
|
||||
string memory imageURL,
|
||||
string memory category,
|
||||
uint cost,
|
||||
uint expiresAt
|
||||
) public returns (bool) {
|
||||
require(bytes(title).length > 0, "Judul tidak boleh kosong");
|
||||
require(bytes(description).length > 0, "Deskripsi tidak boleh kosong");
|
||||
require(bytes(imageURL).length > 0, "GambarURL tidak boleh kosong");
|
||||
require(bytes(category).length > 0, "Kategori tidak boleh kosong");
|
||||
require(cost > 0 ether, "Biaya tidak boleh nol");
|
||||
|
||||
projectStruct memory project;
|
||||
project.id = projectCount;
|
||||
project.owner = msg.sender;
|
||||
project.title = title;
|
||||
project.description = description;
|
||||
project.imageURL = imageURL;
|
||||
project.category = category;
|
||||
project.cost = cost;
|
||||
project.timestamp = block.timestamp;
|
||||
project.expiresAt = expiresAt;
|
||||
|
||||
projects.push(project);
|
||||
projectExist[projectCount] = true;
|
||||
projectsOf[msg.sender].push(project);
|
||||
stats.totalProjects += 1;
|
||||
categoryProjects[category].push(projectCount);
|
||||
|
||||
emit Action (
|
||||
projectCount++,
|
||||
"PROJECT DIBUAT",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateProject(
|
||||
uint id,
|
||||
string memory title,
|
||||
string memory description,
|
||||
string memory imageURL,
|
||||
uint expiresAt
|
||||
) public returns (bool) {
|
||||
require(msg.sender == projects[id].owner, "Entitas yang Tidak Berwenang");
|
||||
require(bytes(title).length > 0, "Judul tidak boleh kosong");
|
||||
require(bytes(description).length > 0, "Deskripsi tidak boleh kosong");
|
||||
require(bytes(imageURL).length > 0, "GambarURL tidak boleh kosong");
|
||||
|
||||
projects[id].title = title;
|
||||
projects[id].description = description;
|
||||
projects[id].imageURL = imageURL;
|
||||
projects[id].expiresAt = expiresAt;
|
||||
|
||||
checkAndRefund(id); // Memeriksa dan mengembalikan dana jika perlu
|
||||
|
||||
emit Action (
|
||||
id,
|
||||
"PROJECT UPDATE",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function deleteProject(uint id) public returns (bool) {
|
||||
require(projects[id].status == statusEnum.OPEN, "Project tidak lagi dibuka");
|
||||
require(msg.sender == projects[id].owner, "Entitas yang Tidak Berwenang");
|
||||
|
||||
projects[id].status = statusEnum.DELETED;
|
||||
performRefund(id);
|
||||
|
||||
emit Action (
|
||||
id,
|
||||
"PROJECT DIHAPUS",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function performRefund(uint id) internal {
|
||||
for(uint i = 0; i < backersOf[id].length; i++) {
|
||||
address _owner = backersOf[id][i].owner;
|
||||
uint _contribution = backersOf[id][i].contribution;
|
||||
|
||||
backersOf[id][i].refunded = true;
|
||||
backersOf[id][i].timestamp = block.timestamp;
|
||||
payTo(_owner, _contribution);
|
||||
|
||||
stats.totalBacking -= 1;
|
||||
stats.totalDonations -= _contribution;
|
||||
}
|
||||
}
|
||||
|
||||
function backProject(uint id) public payable returns (bool) {
|
||||
require(msg.value > 0 ether, "Ether harus lebih besar dari nol");
|
||||
require(projectExist[id], "Project tidak ditemukan");
|
||||
require(projects[id].status == statusEnum.OPEN, "Project tidak lagi dibuka");
|
||||
|
||||
stats.totalBacking += 1;
|
||||
stats.totalDonations += msg.value;
|
||||
projects[id].raised += msg.value;
|
||||
projects[id].backers += 1;
|
||||
|
||||
backersOf[id].push(
|
||||
backerStruct(
|
||||
msg.sender,
|
||||
msg.value,
|
||||
block.timestamp,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
emit Action (
|
||||
id,
|
||||
"DUKUNGAN PROJECT",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
checkAndRefund(id); // Memeriksa dan mengembalikan dana jika perlu
|
||||
|
||||
if(projects[id].raised >= projects[id].cost) {
|
||||
projects[id].status = statusEnum.APPROVED;
|
||||
balance += projects[id].raised;
|
||||
performPayout(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkAndRefund(uint id) internal {
|
||||
if(block.timestamp >= projects[id].expiresAt && projects[id].raised < projects[id].cost) {
|
||||
projects[id].status = statusEnum.REVERTED;
|
||||
performRefund(id);
|
||||
}
|
||||
}
|
||||
|
||||
function performPayout(uint id) internal {
|
||||
uint raised = projects[id].raised;
|
||||
uint tax = (raised * projectTax) / 100;
|
||||
|
||||
projects[id].status = statusEnum.PAIDOUT;
|
||||
|
||||
payTo(projects[id].owner, (raised - tax));
|
||||
payTo(owner, tax);
|
||||
|
||||
balance -= projects[id].raised;
|
||||
|
||||
emit Action (
|
||||
id,
|
||||
"PROJECT DIBAYARKAN",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
function requestRefund(uint id) public returns (bool) {
|
||||
require(
|
||||
projects[id].status != statusEnum.REVERTED ||
|
||||
projects[id].status != statusEnum.DELETED,
|
||||
"Project tidak ditandai sebagai kembalikan atau hapus"
|
||||
);
|
||||
|
||||
projects[id].status = statusEnum.REVERTED;
|
||||
performRefund(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function payOutProject(uint id) public returns (bool) {
|
||||
require(projects[id].status == statusEnum.APPROVED, "Project tidak DISETUJUI");
|
||||
require(
|
||||
msg.sender == projects[id].owner ||
|
||||
msg.sender == owner,
|
||||
"Entitas yang Tidak Berwenang"
|
||||
);
|
||||
|
||||
performPayout(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function changeTax(uint _taxPct) public ownerOnly {
|
||||
projectTax = _taxPct;
|
||||
}
|
||||
|
||||
function getProject(uint id) public view returns (projectStruct memory) {
|
||||
require(projectExist[id], "Project tidak ditemukan");
|
||||
|
||||
return projects[id];
|
||||
}
|
||||
|
||||
function getProjects() public view returns (projectStruct[] memory) {
|
||||
return projects;
|
||||
}
|
||||
|
||||
function getProjectsByCategory(string memory category) public view returns (projectStruct[] memory) {
|
||||
projectStruct[] memory categoryProjectsList = new projectStruct[](categoryProjects[category].length);
|
||||
uint[] memory projectIds = categoryProjects[category];
|
||||
for (uint i = 0; i < projectIds.length; i++) {
|
||||
categoryProjectsList[i] = projects[projectIds[i]];
|
||||
}
|
||||
return categoryProjectsList;
|
||||
}
|
||||
|
||||
function getBackers(uint id) public view returns (backerStruct[] memory) {
|
||||
return backersOf[id];
|
||||
}
|
||||
|
||||
function getComments(uint id) public view returns (commentStruct[] memory) {
|
||||
return commentsOf[id];
|
||||
}
|
||||
|
||||
function addComment(uint id, string memory comment) public returns (bool) {
|
||||
require(projectExist[id], "Project tidak ditemukan");
|
||||
require(
|
||||
hasBackedProject(id, msg.sender) ||
|
||||
projects[id].owner == msg.sender,
|
||||
"Hanya pendukung atau pemilik yang dapat berkomentar"
|
||||
);
|
||||
|
||||
commentsOf[id].push(commentStruct({
|
||||
commenter: msg.sender,
|
||||
comment: comment,
|
||||
timestamp: block.timestamp
|
||||
}));
|
||||
|
||||
emit Action (
|
||||
id,
|
||||
"PROJECT COMMENT",
|
||||
msg.sender,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasBackedProject(uint id, address addr) internal view returns (bool) {
|
||||
for (uint i = 0; i < backersOf[id].length; i++) {
|
||||
if (backersOf[id][i].owner == addr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function payTo(address to, uint amount) internal {
|
||||
(bool success,) = payable(to).call{value: amount}("");
|
||||
require(success, "Transaksi gagal");
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
import App from './App'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
import abi from '../abis/src/contracts/Crowdfunding.sol/Crowdfunding.json'
|
||||
import address from '../abis/contractAddress.json'
|
||||
import { getGlobalState, setGlobalState } from '../store'
|
||||
import { ethers } from 'ethers'
|
||||
|
||||
const { ethereum } = window
|
||||
const contractAddress = address.address
|
||||
const contractAbi = abi.abi
|
||||
let tx
|
||||
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
|
||||
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const isWallectConnected = async () => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const accounts = await ethereum.request({ method: 'eth_accounts' })
|
||||
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
|
||||
|
||||
window.ethereum.on('chainChanged', (chainId) => {
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
window.ethereum.on('accountsChanged', async () => {
|
||||
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
|
||||
await isWallectConnected()
|
||||
})
|
||||
|
||||
if (accounts.length) {
|
||||
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
|
||||
} else {
|
||||
alert('Harap sambungkan dompet.')
|
||||
console.log('Tidak ada akun yang ditemukan.')
|
||||
}
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getEtheriumContract = async () => {
|
||||
const connectedAccount = getGlobalState('connectedAccount')
|
||||
|
||||
if (connectedAccount) {
|
||||
const provider = new ethers.providers.Web3Provider(ethereum)
|
||||
const signer = provider.getSigner()
|
||||
const contract = new ethers.Contract(contractAddress, contractAbi, signer)
|
||||
|
||||
return contract
|
||||
} else {
|
||||
return getGlobalState('contract')
|
||||
}
|
||||
}
|
||||
|
||||
const createProject = async ({
|
||||
title,
|
||||
description,
|
||||
imageURL,
|
||||
cost,
|
||||
expiresAt,
|
||||
category,
|
||||
}) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
|
||||
const contract = await getEtheriumContract()
|
||||
cost = ethers.utils.parseEther(cost)
|
||||
tx = await contract.createProject(title, description, imageURL, category, cost, expiresAt)
|
||||
await tx.wait()
|
||||
await loadProjects()
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateProject = async ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
imageURL,
|
||||
expiresAt,
|
||||
}) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
|
||||
const contract = await getEtheriumContract()
|
||||
tx = await contract.updateProject(id, title, description, imageURL, expiresAt)
|
||||
await tx.wait()
|
||||
await loadProject(id)
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteProject = async (id) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const contract = await getEtheriumContract()
|
||||
await contract.deleteProject(id)
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
|
||||
const contract = await getEtheriumContract()
|
||||
const projects = await contract.getProjects()
|
||||
const stats = await contract.stats()
|
||||
|
||||
setGlobalState('stats', structureStats(stats))
|
||||
setGlobalState('projects', structuredProjects(projects))
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadProject = async (id) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const contract = await getEtheriumContract()
|
||||
const project = await contract.getProject(id)
|
||||
const comments = await contract.getComments(id)
|
||||
|
||||
setGlobalState('project', { ...structuredProjects([project])[0], comments: structuredComments(comments) })
|
||||
} catch (error) {
|
||||
alert(JSON.stringify(error.message))
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const backProject = async (id, amount) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const connectedAccount = getGlobalState('connectedAccount')
|
||||
const contract = await getEtheriumContract()
|
||||
amount = ethers.utils.parseEther(amount)
|
||||
|
||||
tx = await contract.backProject(id, {
|
||||
from: connectedAccount,
|
||||
value: amount._hex,
|
||||
})
|
||||
|
||||
await tx.wait()
|
||||
await getBackers(id)
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getBackers = async (id) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const contract = await getEtheriumContract()
|
||||
let backers = await contract.getBackers(id)
|
||||
|
||||
setGlobalState('backers', structuredBackers(backers))
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const payoutProject = async (id) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const connectedAccount = getGlobalState('connectedAccount')
|
||||
const contract = await getEtheriumContract()
|
||||
|
||||
tx = await contract.payOutProject(id, {
|
||||
from: connectedAccount,
|
||||
})
|
||||
|
||||
await tx.wait()
|
||||
await getBackers(id)
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const addComment = async (id, comment) => {
|
||||
try {
|
||||
if (!ethereum) return alert('Silahkan install Metamask')
|
||||
const contract = await getEtheriumContract()
|
||||
|
||||
tx = await contract.addComment(id, comment)
|
||||
await tx.wait()
|
||||
await loadProject(id)
|
||||
} catch (error) {
|
||||
reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const structuredBackers = (backers) =>
|
||||
backers
|
||||
.map((backer) => ({
|
||||
owner: backer.owner.toLowerCase(),
|
||||
refunded: backer.refunded,
|
||||
timestamp: new Date(backer.timestamp.toNumber() * 1000).toJSON(),
|
||||
contribution: parseInt(backer.contribution._hex) / 10 ** 18,
|
||||
}))
|
||||
.reverse()
|
||||
|
||||
const structuredComments = (comments) =>
|
||||
comments
|
||||
.map((comment) => ({
|
||||
commenter: comment.commenter.toLowerCase(),
|
||||
comment: comment.comment,
|
||||
timestamp: new Date(comment.timestamp.toNumber() * 1000).toJSON(),
|
||||
}))
|
||||
.reverse()
|
||||
|
||||
const structuredProjects = (projects) =>
|
||||
projects
|
||||
.map((project) => ({
|
||||
id: project.id.toNumber(),
|
||||
owner: project.owner.toLowerCase(),
|
||||
title: project.title,
|
||||
description: project.description,
|
||||
timestamp: new Date(project.timestamp.toNumber()).getTime(),
|
||||
expiresAt: new Date(project.expiresAt.toNumber()).getTime(),
|
||||
date: toDate(project.expiresAt.toNumber() * 1000),
|
||||
imageURL: project.imageURL,
|
||||
category: project.category,
|
||||
raised: parseInt(project.raised._hex) / 10 ** 18,
|
||||
cost: parseInt(project.cost._hex) / 10 ** 18,
|
||||
backers: project.backers.toNumber(),
|
||||
status: project.status,
|
||||
}))
|
||||
.reverse()
|
||||
|
||||
const toDate = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
const dd = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`
|
||||
const mm =
|
||||
date.getMonth() + 1 > 9 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`
|
||||
const yyyy = date.getFullYear()
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
}
|
||||
|
||||
const structureStats = (stats) => ({
|
||||
totalProjects: stats.totalProjects.toNumber(),
|
||||
totalBacking: stats.totalBacking.toNumber(),
|
||||
totalDonations: parseInt(stats.totalDonations._hex) / 10 ** 18,
|
||||
})
|
||||
|
||||
const reportError = (error) => {
|
||||
console.log(error.message)
|
||||
}
|
||||
|
||||
export {
|
||||
connectWallet,
|
||||
isWallectConnected,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
loadProjects,
|
||||
loadProject,
|
||||
backProject,
|
||||
getBackers,
|
||||
payoutProject,
|
||||
addComment,
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import moment from 'moment';
|
||||
import { createGlobalState } from 'react-hooks-global-state';
|
||||
|
||||
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
|
||||
createModal: 'scale-0',
|
||||
updateModal: 'scale-0',
|
||||
deleteModal: 'scale-0',
|
||||
backModal: 'scale-0',
|
||||
connectedAccount: '',
|
||||
projects: [],
|
||||
project: null,
|
||||
stats: null,
|
||||
backers: [],
|
||||
});
|
||||
|
||||
const truncate = (text, startChars, endChars, maxLength) => {
|
||||
if (text.length > maxLength) {
|
||||
let start = text.substring(0, startChars);
|
||||
let end = text.substring(text.length - endChars, text.length);
|
||||
while (start.length + end.length < maxLength) {
|
||||
start = start + '.';
|
||||
}
|
||||
return start + end;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const daysRemaining = (expiresAt) => {
|
||||
const now = moment();
|
||||
const expiryDate = moment(Number(expiresAt + '000'));
|
||||
|
||||
const timeDifference = expiryDate.diff(now);
|
||||
|
||||
if (timeDifference <= 0) {
|
||||
return '0 hari';
|
||||
}
|
||||
|
||||
const days = expiryDate.diff(now, 'days');
|
||||
return days <= 1 ? `${days + 1} hari` : `${days + 1} hari`;
|
||||
};
|
||||
|
||||
export {
|
||||
useGlobalState,
|
||||
setGlobalState,
|
||||
getGlobalState,
|
||||
truncate,
|
||||
daysRemaining,
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect } from 'react'
|
||||
import AddButton from '../components/AddButton'
|
||||
import CreateProject from '../components/CreateProject'
|
||||
import Hero from '../components/Hero'
|
||||
import Projects from '../components/Projects'
|
||||
import { loadProjects } from '../services/blockchain'
|
||||
import { useGlobalState } from '../store'
|
||||
import Header from '../components/Header'
|
||||
|
||||
const Home = () => {
|
||||
const [projects] = useGlobalState('projects')
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await loadProjects()
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Hero />
|
||||
<Projects projects={projects} />
|
||||
<CreateProject />
|
||||
<AddButton />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import Navbar from '../components/Navbar';
|
||||
import Why from '../components/Why';
|
||||
import Newsletter from '../components/Newsletter';
|
||||
import Ablockchain from '../components/Ablockchain';
|
||||
import Footer from '../components/Footer';
|
||||
import Title from '../components/Title';
|
||||
|
||||
const Homepage = () => {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<Title />
|
||||
<Why />
|
||||
<Newsletter />
|
||||
<Ablockchain />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Homepage
|
|
@ -0,0 +1,38 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import BackProject from '../components/BackProject'
|
||||
import DeleteProject from '../components/DeleteProject'
|
||||
import ProjectBackers from '../components/ProjectBackers'
|
||||
import ProjectDetails from '../components/ProjectDetails'
|
||||
import UpdateProject from '../components/UpdateProject'
|
||||
import { getBackers, loadProject } from '../services/blockchain'
|
||||
import { useGlobalState } from '../store'
|
||||
import Header from '../components/Header'
|
||||
|
||||
const Project = () => {
|
||||
const { id } = useParams()
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [project] = useGlobalState('project')
|
||||
const [backers] = useGlobalState('backers')
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await loadProject(id);
|
||||
await getBackers(id);
|
||||
setLoaded(true);
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
return loaded ? (
|
||||
<>
|
||||
<Header />
|
||||
<ProjectDetails project={project} />
|
||||
<UpdateProject project={project} />
|
||||
<DeleteProject project={project} />
|
||||
<BackProject project={project} />
|
||||
<ProjectBackers backers={backers} />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default Project
|
24
yarn.lock
|
@ -5659,6 +5659,11 @@ clone@2.1.2, clone@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
|
||||
|
||||
clsx@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -13347,6 +13352,20 @@ react-scripts@5.0.0:
|
|||
optionalDependencies:
|
||||
fsevents "^2.3.2"
|
||||
|
||||
react-toastify@^10.0.5:
|
||||
version "10.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e"
|
||||
integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==
|
||||
dependencies:
|
||||
clsx "^2.1.0"
|
||||
|
||||
react-typed@^2.0.12:
|
||||
version "2.0.12"
|
||||
resolved "https://registry.yarnpkg.com/react-typed/-/react-typed-2.0.12.tgz#898037767a59c43d6e0a4a4e42d13c0adb34abfc"
|
||||
integrity sha512-sn59lrb4NKgvQLDGBRWM4wRNEz5Fp37F0BrLA2cKUmDqPSyQJOOiFigz+6wJof0o99QoE16nVCa3B3ID34ejsg==
|
||||
dependencies:
|
||||
typed.js "^2.1.0"
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
|
@ -15342,6 +15361,11 @@ typed-array-length@^1.0.4:
|
|||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typed.js@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/typed.js/-/typed.js-2.1.0.tgz#b97a06111a5f57d6a59acfdd36d4f9b891ee13b4"
|
||||
integrity sha512-bDuXEf7YcaKN4g08NMTUM6G90XU25CK3bh6U0THC/Mod/QPKlEt9g/EjvbYB8x2Qwr2p6J6I3NrsoYaVnY6wsQ==
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
|
|