diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..66cc3011 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@next/next/recommended" + ], + "rules": { + "react/prop-types": 0, + "indent": ["error", 2], + "linebreak-style": 1, + "quotes": ["error", "double"], + "semi": ["error", "always"] + }, + "plugins": ["react"], + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es2021": true, + "browser": true, + "node": true + }, + "settings": { + "react": { + "version": "detect" + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 483d58f5..99ff53f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ dist docs node_modules -.env env # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..7239117e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "pwa-chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + } + ] +} \ No newline at end of file diff --git a/components/Buy.js b/components/Buy.js new file mode 100644 index 00000000..8e4365cf --- /dev/null +++ b/components/Buy.js @@ -0,0 +1,135 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { Keypair, Transaction } from "@solana/web3.js"; +import { findReference, FindReferenceError } from "@solana/pay"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import { ThreeDots } from "react-loader-spinner"; +import IPFSDownload from "./IpfsDownload"; +import { addOrder, fetchItem, hasPurchased } from "../lib/api"; + +const STATUS = { + Initial: "Initial", + Submitted: "Submitted", + Paid: "Paid", +}; + +export default function Buy({ itemID }) { + const { connection } = useConnection(); + const { publicKey, sendTransaction } = useWallet(); + const orderID = useMemo(() => Keypair.generate().publicKey, []); // Public key used to identify the order + + const [item, setItem] = useState(null); // IPFS hash & filename of the purchased item + const [loading, setLoading] = useState(true); // Loading state of all above + const [status, setStatus] = useState(STATUS.Initial); // Tracking transaction status + + // useMemo is a React hook that only computes the value if the dependencies change + const order = useMemo( + () => ({ + buyer: publicKey.toString(), + orderID: orderID.toString(), + itemID: itemID, + }), + [publicKey, orderID, itemID] + ); + + // Fetch the transaction object from the server + const processTransaction = async () => { + setLoading(true); + try { + const txResponse = await fetch("../api/createTransaction", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(order), + }); + console.log(txResponse); + const txData = await txResponse.json(); + + // We create a transaction object + const tx = Transaction.from(Buffer.from(txData.transaction, "base64")); + console.log("Tx data is", tx); + + // Attempt to send the transaction to the network + // Send the transaction to the network + const txHash = await sendTransaction(tx, connection); + console.log(`Transaction sent: https://solscan.io/tx/${txHash}?cluster=devnet`); + setStatus(STATUS.Submitted); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + // Check if this address already has already purchased this item + // If so, fetch the item and set paid to true + // Async function to avoid blocking the UI + async function checkPurchased() { + setLoading(true); + const purchased = await hasPurchased(publicKey, itemID); + if (purchased) { + getItem(itemID); + } + setLoading(false); + } + checkPurchased(); + }, [publicKey, itemID]); + + useEffect(() => { + // Check if transaction was confirmed + if (status === STATUS.Submitted) { + setLoading(true); + const interval = setInterval(async () => { + try { + const result = await findReference(connection, orderID); + console.log("Finding tx reference", result.confirmationStatus); + if (result.confirmationStatus === "confirmed" || result.confirmationStatus === "finalized") { + clearInterval(interval); + addOrder(order); + getItem(itemID); + // alert('Thank you for your purchase!'); + } + } catch (e) { + if (e instanceof FindReferenceError) { + return null; + } + console.error("Unknown error", e); + } finally { + setLoading(false); + } + }, 1000); + return () => { + clearInterval(interval); + }; + } + }, [status]); + + async function getItem(itemID) { + setStatus(STATUS.Paid); + const item = await fetchItem(itemID); + setItem(item); + } + + if (!publicKey) { + return ( +
+

You need to connect your wallet to make transactions

+
+ ); + } else if (loading || (status === STATUS.Paid && !item)) { + return ; + } else { + return ( +
+ {item ? ( + + ) : ( + + )} +
+ ); + } +} diff --git a/components/CreateProduct.js b/components/CreateProduct.js new file mode 100644 index 00000000..1cfd1857 --- /dev/null +++ b/components/CreateProduct.js @@ -0,0 +1,143 @@ +import React, { useState } from "react"; +import { create } from "ipfs-http-client"; +import styles from "../styles/CreateProduct.module.css"; + +const client = create("https://ipfs.infura.io:5001/api/v0"); + +const CreateProduct = () => { + const [newProduct, setNewProduct] = useState({ + name: "", + price: "", + image_url: "", + description: "", + }); + const [file, setFile] = useState({}); + const [uploading, setUploading] = useState(false); + const [uploaded, setUploaded] = useState(false); + + // USE FOR TESTING + // function sleep(ms) { + // return new Promise(resolve => setTimeout(resolve, ms)); + // } + + async function onChange(e) { + setUploaded(false); + setUploading(true); + const files = e.target.files; + try { + setFile({}); + console.log(files[0]); + + const added = await client.add(files[0]); + // USE FOR TESTING + // const added = { path:"test" }; + // await sleep(5000); + + console.log("file added : ", files[0].name, added.path); + setFile({ filename: files[0].name, hash: added.path }); + } catch (error) { + console.log("Error uploading file: ", error); + } finally { + setUploaded(true); + setUploading(false); + } + } + + const createProduct = async () => { + try { + // Combine product data and file.name + const product = { ...newProduct, ...file }; + console.log("Sending product to api", product); + const response = await fetch("../api/addProduct", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(product), + }); + const data = await response.json(); + if (response.status === 200) { + alert("Product added!"); + } else { + alert("Unable to add product: ", data.error); + } + } catch (error) { + console.log(error); + } + }; + + const price = "0.01" + process.env.NEXT_PUBLIC_CURRENCY; + + return ( +
+
+
+
+

Create Product

+
+ +
+ + {/* {file && file.filename &&

{file.filename}

} */} +
+ { + setNewProduct({ ...newProduct, name: e.target.value }); + }} + /> + { + setNewProduct({ ...newProduct, price: e.target.value }); + }} + /> +
+ +
+ { + setNewProduct({ ...newProduct, image_url: e.target.value }); + }} + /> +
+