diff --git a/.eslintrc b/.eslintrc
index 98352ca..d1179aa 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,11 +1,13 @@
{
+ "parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
- "standard",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
"prettier",
"next/core-web-vitals"
],
@@ -16,7 +18,7 @@
"ecmaVersion": 12,
"sourceType": "module"
},
- "plugins": ["react"],
+ "plugins": ["react","@typescript-eslint"],
"rules": {
"@next/next/no-img-element": "off"
}
diff --git a/components/InputFieldError.tsx b/components/InputFieldError.tsx
new file mode 100644
index 0000000..6cc8bb5
--- /dev/null
+++ b/components/InputFieldError.tsx
@@ -0,0 +1,9 @@
+
+interface InputFieldErrorProps {
+ message?: string
+}
+
+export const InputFieldError = ({ message }: InputFieldErrorProps) => {
+ if (!message) return null
+ return
{message}
+}
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..4f11a03
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/package.json b/package.json
index 980070b..161fad3 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,14 @@
"dev": "next dev",
"build": "next build",
"start": "export PORT=4006 next start",
- "lint": "next lint"
+ "lint": "next lint --fix"
},
"dependencies": {
"@headlessui/react": "^1.4.2",
"@heroicons/react": "^1.0.5",
+ "@hookform/resolvers": "^2.8.5",
+ "@openzeppelin/contracts": "^4.4.1",
+ "@react-hookz/web": "^12.0.4",
"@textile/eth-storage": "^1.0.0",
"axios": "^0.24.0",
"ethers": "^5.5.2",
@@ -22,16 +25,27 @@
"nft.storage": "^5.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-hook-form": "^7.22.5",
"react-markdown": "^7.1.1",
- "web3modal": "^1.9.4"
+ "web3modal": "^1.9.4",
+ "yup": "^0.32.11"
},
"devDependencies": {
+ "@nomiclabs/hardhat-ethers": "^2.0.3",
+ "@nomiclabs/hardhat-waffle": "^2.0.1",
+ "@types/react": "^17.0.38",
+ "@typescript-eslint/eslint-plugin": "^5.8.1",
+ "@typescript-eslint/parser": "^5.8.1",
"autoprefixer": "^10.4.0",
- "eslint": "^7.32.0",
+ "chai": "^4.3.4",
+ "eslint": "^8.5.0",
"eslint-config-next": "^12.0.7",
"eslint-config-prettier": "^8.3.0",
+ "ethereum-waffle": "^3.4.0",
+ "hardhat": "^2.8.0",
"postcss": "^8.4.5",
"prettier": "2.5.1",
- "tailwindcss": "^3.0.7"
+ "tailwindcss": "^3.0.7",
+ "typescript": "^4.5.4"
}
}
diff --git a/pages/create.js b/pages/create.js
deleted file mode 100644
index 50bc3cd..0000000
--- a/pages/create.js
+++ /dev/null
@@ -1,260 +0,0 @@
-import { useState } from "react"
-import { ethers } from "ethers"
-import { create as ipfsHttpClient } from "ipfs-http-client"
-import { useRouter } from "next/router"
-import axios from "axios"
-import { NFTStorage } from "nft.storage"
-
-const client = ipfsHttpClient("https://ipfs.infura.io:5001/api/v0")
-import { nftaddress, nftmarketaddress } from "../config"
-
-import NFT from "../artifacts/contracts/NFT.sol/NFT.json"
-import Market from "../artifacts/contracts/Market.sol/NFTMarket.json"
-
-let ethAccount
-export default function CreateItem() {
- const [publishstate, setPublishstate] = useState("")
- const [fileUrl, setFileUrl] = useState(null)
- const [afile, setAFile] = useState(null)
- const [submitted, setSubmitted] = useState(false)
-
- const [formInput, updateFormInput] = useState({
- price: "",
- name: "",
- description: "",
- s_tags: "",
- names: "",
- })
- const router = useRouter()
- console.log(router)
- if (router.pathname == "/create") {
- if (typeof document !== "undefined") {
- var els = document.getElementsByClassName("_nav")
- Array.prototype.forEach.call(els, function (el) {
- el.classList.remove("current")
- })
- document.getElementById("_create").classList.add("current")
- }
- }
-
- if (typeof window !== "undefined") {
- ethAccount = localStorage.getItem("ethAccount")
- if (!ethAccount) {
- alert("No ETH Account, Please login")
- router.push("/articles-all")
- }
- }
- console.log(ethAccount)
-
- async function onChange(e) {
- const file = e.target.files[0]
- console.log(file)
- console.log("file will be uploaded")
- try {
- const added = await client.add(file, {
- progress: (prog) => console.log(`received: ${prog}`),
- })
- alert("file is uploaded!")
- const url = `https://ipfs.infura.io/ipfs/${added.path}`
- console.log(url)
- console.log(added) // added.size file.size
- setAFile(file) // file.name file.type "image/png"
- setFileUrl(url)
- } catch (error) {
- console.log("Error uploading file: ", error)
- alert("Error uploading file: ", error)
- }
- }
- async function PublishIt(e) {
- if (submitted) {
- console.log("already submitted")
- return
- }
- setSubmitted(true)
- e.preventDefault()
- console.log("submitted!")
- setPublishstate("ing...")
- const { name, description, s_tags, names } = formInput
- if (!afile) {
- alert("Please upload FEATURED IMAGE")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- const _filename = afile.name.split(".")
- console.log(afile.type, _filename[_filename.length - 1])
- const filetype = afile.type
- const filesize = afile.size
- const filename = afile.name
- if (!name) {
- alert("Title is not optional")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- if (!description) {
- alert("Content is not optional")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- if (!s_tags) {
- alert("Tags is not optional")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- if (!fileUrl) {
- alert("Feature Image is not optional")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- if (!names) {
- alert("Authors Name is not optional")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- if (!ethAccount) {
- alert("No ETH Account, Please login")
- setPublishstate("")
- setSubmitted(false)
- return
- }
- /* first, upload metadata to IPFS */
- const license = "CC-BY-SA"
- const license_url = "https://creativecommons.org/licenses/by-sa/4.0/"
- const tags = s_tags.split(" ")
- const authors = [
- {
- name: names,
- wallet: {
- eth: ethAccount,
- },
- },
- ]
- const data = JSON.stringify({
- name,
- description,
- image: fileUrl,
- license,
- license_url,
- filesize,
- filename,
- filetype,
- tags,
- authors,
- })
-
- // const endpoint = 'https://api.nft.storage' // the default
- const API_TOKEN =
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnaXRodWJ8MTQ3Mjg1MCIsImlzcyI6Im5mdC1zdG9yYWdlIiwiaWF0IjoxNjE4ODQ0ODAwOTgzLCJuYW1lIjoiZGVmYXVsdCJ9.bdDjCtOaANp49ysENB4-4xpVrhDRbdeqV39t5aVYsjo" // your API key from https://nft.storage/manage
- try {
- // const store = new NFTStorage({ endpoint, token })
- const client = new NFTStorage({ token: API_TOKEN })
- const cid = await client.storeBlob(new Blob([data]))
- const status = await client.status(cid)
- console.log(cid)
- console.log(status)
- } catch (error) {
- console.log("Error uploading to nft.storage: ", error)
- }
-
- try {
- const added = await client.add(data)
- console.log(added)
- const url = `https://ipfs.infura.io/ipfs/${added.path}`
- // We also store metadata in Dweb Search
- console.log(s_tags)
- console.log(names)
- // const dweb_search_url = `https://dweb-search-api.anwen.cc/add_meta?path=${added.path}ð=${ethAccount}&name=${name}&image=${fileUrl}&tags=${s_tags}&authors=${names}`
- const dweb_search_url = `https://dweb-search-api.anwen.cc/add_meta`
- console.log(dweb_search_url)
-
- // ?path=${added.path}ð=${ethAccount}&name=${name}&image=${fileUrl}&tags=${s_tags}&authors=${names}`
-
- const ret = await axios.post(dweb_search_url, {
- path: added.path,
- eth: ethAccount,
- name: name,
- image: fileUrl,
- tags: s_tags,
- authors: names
- }) // TODO
- console.log(ret)
- if (ret.status == 200 && !('error' in ret.data)) {
- router.push("/articles-my")
- } else {
- let err = ret.data['error']
- console.log(err)
- alert(`Sorry! Publish failed, server error: ${err}. We're fixing. You can try later.`)
- setPublishstate("")
- setSubmitted(false)
- return
- }
- /* after file is uploaded to IPFS, pass the URL to save it on Polygon */
- // createNFT(url)
- } catch (error) {
- console.log("Error uploading to dweb-search: ", error)
- alert("Sorry! Publish failed, server error. we are fixing...")
- }
- }
-
- return (
-
-
-
Attention:
-
- - All your published data and metadata is open to public and with{" "}
- CC-BY-SA{" "}
- License.{" "}
-
-
- - They will be on IPFS and Dweb Search Engine too.
-
-
- - It’s forbidden to mint anything which doesn’t belong to you.
-
-
- updateFormInput({ ...formInput, name: e.target.value })
- }
- />
-
-
- )
-}
diff --git a/pages/create.tsx b/pages/create.tsx
new file mode 100644
index 0000000..308da52
--- /dev/null
+++ b/pages/create.tsx
@@ -0,0 +1,189 @@
+import {useRouter} from "next/router"
+import axios from "axios"
+import {useForm} from "react-hook-form";
+import {yupResolver} from '@hookform/resolvers/yup';
+import * as yup from "yup";
+import {addNFTToNFTStorage} from "../services/NFTStorage";
+import {addToIPFS} from "../services/IPFSHttpClient";
+
+import {nftaddress, nftmarketaddress} from "../config"
+import NFT from "../artifacts/contracts/NFT.sol/NFT.json"
+import Market from "../artifacts/contracts/Market.sol/NFTMarket.json"
+import {useEffect, useState} from "react";
+import {InputFieldError} from "../components/InputFieldError";
+
+interface IFormInputs {
+ price: string
+ name: string
+ description: string
+ s_tags: string
+ author: string
+ files: FileList
+}
+
+const schema = yup.object({
+ price: yup.string(),
+ name: yup.string().required('Title is not optional'),
+ description: yup.string().required("Content is not optional"),
+ s_tags: yup.string().required("Tags is not optional"),
+ author: yup.string().required("Authors Name is not optional"),
+ files: yup.mixed().test({ test: (value) => value.length, message: "Feature Image is not optional"}),
+}).required();
+
+export default function CreateItem() {
+ const router = useRouter()
+ // TODO: create useAccount Hook and listen account change
+ const [ethAccount, setEthAccount] = useState()
+ const [preview, setPreview] = useState()
+
+ // todo: remove this menu active track
+ if (router.pathname == "/create") {
+ if (typeof document !== "undefined") {
+ const els = document.getElementsByClassName("_nav")
+ Array.prototype.forEach.call(els, function (el) {
+ el.classList.remove("current")
+ })
+ document.getElementById("_create")?.classList?.add("current")
+ }
+ }
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const account = localStorage.getItem("ethAccount")
+ if (!account) {
+ alert("No ETH Account, Please login")
+ router.push("/articles-all")
+ return
+ }
+ setEthAccount(account)
+ }
+ }, [])
+
+ const {register, handleSubmit, formState: {errors, isSubmitting}, watch} = useForm({
+ resolver: yupResolver(schema)
+ });
+ const watchedFiles = watch("files", null);
+
+ useEffect(() => {
+ if (!watchedFiles) return
+ if (!watchedFiles[0]) return
+
+ const url = URL.createObjectURL(watchedFiles[0])
+ setPreview(url)
+ }, [watchedFiles])
+
+ const onSubmit = async (data: IFormInputs) => {
+ const file = data.files[0]
+ const {type: filetype, size: filesize, name: filename} = file
+ const addedImage = await addToIPFS(file)
+ const imageURL = `https://ipfs.infura.io/ipfs/${addedImage.path}`
+ const license = "CC-BY-SA"
+ const license_url = "https://creativecommons.org/licenses/by-sa/4.0/"
+ const tags = data.s_tags.split(" ")
+ const authors = [{
+ name: data.author,
+ wallet: {
+ eth: ethAccount,
+ },
+ }]
+ const nftData = JSON.stringify({
+ name: data.name,
+ description: data.description,
+ image: imageURL,
+ license,
+ license_url,
+ filesize,
+ filename,
+ filetype,
+ tags,
+ authors,
+ })
+
+ await addNFTToNFTStorage(nftData)
+
+ const addedNFT = await addToIPFS(nftData)
+ // TODO: use this url?
+ const url = `https://ipfs.infura.io/ipfs/${addedNFT.path}`
+ // TODO: need fix this url?
+ const dweb_search_url = `https://dweb-search-api.anwen.cc/add_meta`
+ const ret = await axios.post(dweb_search_url, {
+ path: addedNFT.path,
+ eth: ethAccount,
+ name: data.name,
+ image: imageURL,
+ tags: data.s_tags,
+ authors: data.author
+ }) // TODO
+ if (ret.status == 200 && !('error' in ret.data)) {
+ router.push("/articles-my")
+ } else {
+ const err = ret.data['error']
+ throw new Error(err)
+ }
+ }
+
+ const onError = (error) => {
+ console.log("Error uploading to dweb-search: ", error)
+ alert("Sorry! Publish failed, server error. we are fixing...")
+ }
+
+ return (
+
+ )
+}
diff --git a/services/IPFSHttpClient.ts b/services/IPFSHttpClient.ts
new file mode 100644
index 0000000..7a38f18
--- /dev/null
+++ b/services/IPFSHttpClient.ts
@@ -0,0 +1,13 @@
+import { create as ipfsHttpClient } from "ipfs-http-client"
+
+// TODO: remove this ignore
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+const client = ipfsHttpClient("https://ipfs.infura.io:5001/api/v0")
+
+// TODO: remove any
+export async function addToIPFS(data: any) {
+ return client.add(data)
+}
+
+
diff --git a/services/NFTStorage.ts b/services/NFTStorage.ts
new file mode 100644
index 0000000..617ea08
--- /dev/null
+++ b/services/NFTStorage.ts
@@ -0,0 +1,13 @@
+import {NFTStorage} from "nft.storage";
+
+const API_TOKEN =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnaXRodWJ8MTQ3Mjg1MCIsImlzcyI6Im5mdC1zdG9yYWdlIiwiaWF0IjoxNjE4ODQ0ODAwOTgzLCJuYW1lIjoiZGVmYXVsdCJ9.bdDjCtOaANp49ysENB4-4xpVrhDRbdeqV39t5aVYsjo" // your API key from https://nft.storage/manage
+
+export async function addNFTToNFTStorage(data: string) {
+ // const endpoint = 'https://api.nft.storage' // the default
+ const client = new NFTStorage({token: API_TOKEN})
+
+ const cid = await client.storeBlob(new Blob([data]))
+ const status = await client.status(cid)
+ return { cid, status }
+}
\ No newline at end of file
diff --git a/services/backend.ts b/services/backend.ts
new file mode 100644
index 0000000..e69de29
diff --git a/tailwind.config.js b/tailwind.config.js
index 8165888..6d01a48 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,6 +1,6 @@
module.exports = {
purge: {
- content: ["./pages/*.js"],
+ content: ["./pages/*.js", "./pages/*.tsx", "./components/*.tsx"],
},
darkMode: "class", // false or 'media' or 'class'
theme: {
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6db37c0
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve"
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}