Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some updates to the project w/ One crash fix #7

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
dist
docs
node_modules
.env
env
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

Expand Down
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
135 changes: 135 additions & 0 deletions components/Buy.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<p>You need to connect your wallet to make transactions</p>
</div>
);
} else if (loading || (status === STATUS.Paid && !item)) {
return <ThreeDots color="#4664ff" height={45} width={80} />;
} else {
return (
<div>
{item ? (
<IPFSDownload hash={item.hash} filename={item.filename} />
) : (
<button className="buy-button" onClick={processTransaction}>
Buy now 🠚
</button>
)}
</div>
);
}
}
143 changes: 143 additions & 0 deletions components/CreateProduct.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.background_blur}>
<div className={styles.create_product_container}>
<div className={styles.create_product_form}>
<header className={styles.header}>
<h1>Create Product</h1>
</header>

<div className={styles.form_container}>
<input
className={styles.input}
type="file"
accept=".zip,.rar,.7zip"
placeholder="File"
disabled={uploading}
required={uploaded}
onChange={onChange}
/>
{/* {file && file.filename && <p className="file-name">{file.filename}</p>} */}
<div className={styles.flex_row}>
<input
className={styles.input}
type="text"
placeholder="Product Name"
onChange={(e) => {
setNewProduct({ ...newProduct, name: e.target.value });
}}
/>
<input
className={styles.input}
type="text"
placeholder={price}
onChange={(e) => {
setNewProduct({ ...newProduct, price: e.target.value });
}}
/>
</div>

<div className={styles.flex_row}>
<input
className={styles.input}
type="url"
placeholder="Image URL ex: https://i.imgur.com/rVD8bjt.png"
onChange={(e) => {
setNewProduct({ ...newProduct, image_url: e.target.value });
}}
/>
</div>
<textarea
className={styles.text_area}
placeholder="Description here..."
onChange={(e) => {
setNewProduct({ ...newProduct, description: e.target.value });
}}
/>

<button
className={styles.button}
onClick={() => {
createProduct();
}}
disabled={uploading}
>
Create Product
</button>
</div>
</div>
</div>
</div>
);
};

export default CreateProduct;
21 changes: 21 additions & 0 deletions components/IpfsDownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import useIPFS from "../hooks/useIPFS";

const IPFSDownload = ({ hash, filename }) => {

const file = useIPFS(hash, filename);

return (
<div>
{file ? (
<div className="download-component">
<a className="download-button" href={file} download={filename}>Download</a>
</div>
) : (
<p>Downloading file...</p>
)}
</div>
);
};

export default IPFSDownload;
Loading