Skip to content

Commit 62d2264

Browse files
committed
feat: new ui for create page
1 parent 52cdb60 commit 62d2264

File tree

9 files changed

+365
-203
lines changed

9 files changed

+365
-203
lines changed

.eslintrc.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"space-before-blocks": "error",
66
"space-before-function-paren": [2, "never"],
77
"object-curly-spacing": ["error", "always"],
8-
"indent": ["error", 2]
8+
"indent": ["error", 2],
9+
"max-len": ["error", {
10+
"code": 120,
11+
"ignorePattern": "className"
12+
}]
913
}
1014
}

components/Editor.tsx

+336-19
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,349 @@
1-
import { memo } from "react"
1+
import { Fragment, memo, useEffect, useState } from "react"
22
import { Tiptap } from "./Tiptap"
33
import TextareaAutosize from 'react-textarea-autosize'
4+
import * as yup from "yup"
5+
import { useForm } from "react-hook-form"
6+
import { yupResolver } from '@hookform/resolvers/yup'
7+
import { OnChangeValue } from "react-select"
8+
import CreatableSelect from "react-select/creatable"
9+
import { useLocalStorageValue } from "@react-hookz/web"
10+
import { CREATE_CACHE, CREATE_USED_AUTHORS, CREATE_USED_TAGS } from "../constants"
11+
import { addToIPFS } from "../services/IPFSHttpClient"
12+
import { addNFTToNFTStorage } from "../services/NFTStorage"
13+
import axios from "axios"
14+
import router from "next/router"
15+
import { Dialog, Menu, Transition } from '@headlessui/react'
16+
import { ExclamationIcon } from "@heroicons/react/outline"
417

518
interface EditorProps {
619
account: string
720
}
821

22+
interface IFormInputs {
23+
price: string
24+
name: string
25+
description: string
26+
s_tags: string
27+
author: string
28+
files: FileList
29+
}
30+
const customStyles = {
31+
menu: (provided, state) => ({
32+
...provided,
33+
width: '200px',
34+
borderBottom: '1px dotted pink',
35+
color: state.selectProps.menuColor,
36+
padding: '5px 10px',
37+
}),
38+
control: (provided, state) => ({
39+
...provided,
40+
border: 'none',
41+
boxShadow: 'none'
42+
}),
43+
valueContainer: (provided, state) => ({
44+
...provided,
45+
}),
46+
dropdownIndicator: (provided, state) => ({
47+
...provided,
48+
display: 'none'
49+
}),
50+
indicatorsContainer: (provided, state) => ({
51+
...provided,
52+
display: 'none'
53+
}),
54+
multiValue: (provided, state) => {
55+
const opacity = state.isDisabled ? 0.5 : 1
56+
const transition = 'opacity 300ms'
57+
58+
return {
59+
...provided,
60+
opacity,
61+
transition,
62+
background: 'transparent',
63+
borderRadius: '100px',
64+
'& > div': {
65+
color: 'rgb(107 114 128 / var(--tw-text-opacity))',
66+
fontSize: '0.875rem',
67+
lineHeight: '1.25rem',
68+
}
69+
}
70+
}
71+
}
72+
73+
74+
const schema = yup.object({
75+
price: yup.string(),
76+
name: yup.string().required('Title is not optional'),
77+
description: yup.string().required("Content is not optional"),
78+
s_tags: yup.string().required("Tags is not optional"),
79+
author: yup.string().required("Authors Name is not optional"),
80+
files: yup.mixed().test({ test: (value) => value.length, message: "Feature Image is not optional" }),
81+
}).required()
82+
83+
type Option = { label: string, value: string, __isNew__: boolean }
984
export const Editor = memo<EditorProps>(({ account }) => {
85+
const [isOpen, setIsOpen] = useState(false)
86+
const [cachedTags, setCachedTags] = useState<Option[]>([])
87+
const [cachedAuthors, setCachedAuthors] = useState<Option>()
88+
const [tagsInLocal, setLocalTags] = useLocalStorageValue<Omit<Option, '__isNew__'>[]>(CREATE_USED_TAGS)
89+
const [authorsInLocal, setLocalAuthors] = useLocalStorageValue<Omit<Option, '__isNew__'>[]>(CREATE_USED_AUTHORS)
90+
const [preview, setPreview] = useState<string>()
91+
const {
92+
register,
93+
handleSubmit,
94+
setValue,
95+
formState: { errors, isSubmitting, isValid },
96+
watch,
97+
trigger
98+
} = useForm<IFormInputs>({
99+
resolver: yupResolver(schema)
100+
})
101+
102+
// update preview image
103+
const watchedFiles = watch("files", null)
104+
useEffect(() => {
105+
if (!watchedFiles) return
106+
if (!watchedFiles[0]) return
107+
108+
const url = URL.createObjectURL(watchedFiles[0])
109+
setPreview(url)
110+
}, [watchedFiles])
111+
112+
const handleAuthorsChange = (
113+
newValue: Option,
114+
) => {
115+
setValue('author', newValue.value)
116+
setCachedAuthors(newValue)
117+
}
118+
119+
const handleTagsChange = (
120+
newValue: OnChangeValue<Option, true>,
121+
) => {
122+
setValue('s_tags', newValue.map(x => x.value).join(','))
123+
setCachedTags([...newValue])
124+
}
125+
126+
const onSubmit = async(data: IFormInputs) => {
127+
const file = data.files[0]
128+
const { type: filetype, size: filesize, name: filename } = file
129+
const addedImage = await addToIPFS(file)
130+
const imageURL = `https://ipfs.infura.io/ipfs/${addedImage.path}`
131+
const license = "CC-BY-SA"
132+
const license_url = "https://creativecommons.org/licenses/by-sa/4.0/"
133+
const tags = data.s_tags.split(',')
134+
const authors = [{
135+
name: data.author,
136+
wallet: {
137+
eth: account,
138+
},
139+
}]
140+
const nftData = JSON.stringify({
141+
name: data.name,
142+
description: data.description,
143+
image: imageURL,
144+
license,
145+
license_url,
146+
filesize,
147+
filename,
148+
filetype,
149+
tags,
150+
authors,
151+
})
152+
153+
await addNFTToNFTStorage(nftData)
154+
155+
const addedNFT = await addToIPFS(nftData)
156+
// TODO: need fix this url?
157+
const dweb_search_url = `https://dweb-search-api.anwen.cc/add_meta`
158+
const sig_login = localStorage.getItem("sig_login")
159+
const aethAccount = localStorage.getItem("ethAccount")
160+
axios.defaults.headers.common['authorization'] = `Bearer ${sig_login}`
161+
axios.defaults.headers.common['address'] = aethAccount
162+
const ret = await axios.post(dweb_search_url, {
163+
path: addedNFT.path,
164+
eth: account,
165+
name: data.name,
166+
image: imageURL,
167+
tags: data.s_tags,
168+
authors: data.author
169+
}) // TODO
170+
if (ret.status == 200 && !('error' in ret.data)) {
171+
updateLocalCache()
172+
await router.push("/articles-my")
173+
} else {
174+
const err = ret.data['error']
175+
throw new Error(err)
176+
}
177+
}
178+
179+
const updateLocalCache = () => {
180+
const newTags = [
181+
...tagsInLocal ?? [],
182+
...cachedTags.filter(x => x.__isNew__)
183+
].map(x => ({ label: x.label, value: x.value }))
184+
const newAuthors = [
185+
...authorsInLocal ?? [],
186+
cachedAuthors.__isNew__ ? cachedAuthors : undefined
187+
].filter(x => x).map(x => ({ label: x.label, value: x.value }))
188+
setLocalTags(newTags)
189+
setLocalAuthors(newAuthors)
190+
191+
localStorage.removeItem(CREATE_CACHE)
192+
}
193+
194+
const onError = (error) => {
195+
if (Object.keys(error)[0]) {
196+
alert(`Error uploading to dweb-search: ${error[Object.keys(error)[0]].message}`)
197+
} else {
198+
alert("Sorry! Publish failed, server error. we are fixing...")
199+
}
200+
}
201+
10202
return (
11-
<div style={{ width: '720px' }}>
12-
<div>
13-
<span className="ml-2 bg-gray-200 text-gray-500 rounded-full inline-block p-1 px-2 text-sm">+ Add Image</span>
14-
</div>
15-
<div className="pt-12">
16-
<TextareaAutosize placeholder="Give a title" className="border-0 outline-0 text-5xl w-full resize-none" />
17-
</div>
18-
<div className="my-6 text-gray-800 flex items-center">
19-
<div className="w-8 h-8 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
20-
<svg fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
21-
className="w-4 h-4" viewBox="0 0 24 24">
22-
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
23-
<circle cx="12" cy="7" r="4"/>
24-
</svg>
203+
<div className='relative w-full flex justify-center'>
204+
<form style={{ width: '720px' }} onSubmit={handleSubmit(onSubmit, onError)}>
205+
<div className='inline-flex absolute -top-10 right-2 items-center'>
206+
<Menu as="div" className="relative inline-block text-left">
207+
<div>
208+
<Menu.Button className="px-2 flex items-center">
209+
<ExclamationIcon
210+
className="w-8 h-8"
211+
aria-hidden="true"
212+
/>
213+
</Menu.Button>
214+
</div>
215+
<Transition
216+
as={Fragment}
217+
enter="transition ease-out duration-100"
218+
enterFrom="transform opacity-0 scale-95"
219+
enterTo="transform opacity-100 scale-100"
220+
leave="transition ease-in duration-75"
221+
leaveFrom="transform opacity-100 scale-100"
222+
leaveTo="transform opacity-0 scale-95"
223+
>
224+
<Menu.Items
225+
className="absolute right-0 w-80 mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
226+
<div className="px-6 py-8">
227+
<Menu.Item>
228+
<div className="w-full">
229+
<h2>Attention: </h2>
230+
<p className="mt-2">
231+
- All your published data and metadata is open to public and with{" "}
232+
<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a>{" "}
233+
License.{" "}
234+
</p>
235+
<p className="mt-2">
236+
- They will be on IPFS and Dweb Search Engine too.
237+
</p>
238+
<p className="mt-2">
239+
- It’s forbidden to mint anything which doesn’t belong to you.
240+
</p>
241+
</div>
242+
</Menu.Item>
243+
</div>
244+
</Menu.Items>
245+
</Transition>
246+
</Menu>
247+
<button
248+
disabled={isSubmitting}
249+
type="submit"
250+
className="font-bold bg-pink-500 text-white rounded px-4 py-2 cursor-pointer"
251+
onClick={() => {
252+
const cache = JSON.parse(localStorage.getItem(CREATE_CACHE) as any)
253+
if (cache?.content?.[0]?.content) {
254+
setValue('description', JSON.stringify(cache))
255+
trigger()
256+
}
257+
}}
258+
>
259+
{`Publish${isSubmitting ? '...' :''}`}
260+
</button>
261+
</div>
262+
<div>
263+
<label>
264+
{
265+
!preview &&
266+
<div className="inline-flex bg-gray-200 text-gray-500 rounded-full px-2 cursor-pointer">
267+
<svg viewBox="0 0 24 24" className="w-3">
268+
<path
269+
d="M12 1.5v21M1.5 12h21"
270+
stroke="#343F44"
271+
strokeLinecap="round"
272+
strokeLinejoin="round"
273+
fill="none"
274+
fillRule="evenodd"
275+
/>
276+
</svg>
277+
<span className="inline-block p-1 text-sm">
278+
Add feature image
279+
</span>
280+
</div>
281+
}
282+
<input type="file" name="Asset" className="hidden" {...register("files")} />
283+
{
284+
preview &&
285+
<img className="lg:h-48 md:h-36 w-full object-cover object-center cursor-pointer" src={preview} />
286+
}
287+
</label>
25288
</div>
26-
<span className="ml-2 text-gray-800">Author</span>
27-
<span className="ml-2 bg-gray-200 text-gray-500 rounded-full inline-block p-1 px-2 text-sm">0xEddsad</span>
28-
</div>
29-
<Tiptap />
289+
<div className="pt-12">
290+
<TextareaAutosize
291+
placeholder="Give a title"
292+
className="border-0 outline-0 text-5xl w-full resize-none" {...register("name")}
293+
/>
294+
</div>
295+
<div className="my-6 mb-4">
296+
<div className="text-gray-800 flex items-center">
297+
<div className="w-8 h-8 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400">
298+
<svg fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
299+
className="w-4 h-4" viewBox="0 0 24 24">
300+
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/>
301+
<circle cx="12" cy="7" r="4"/>
302+
</svg>
303+
</div>
304+
<span
305+
className="mx-2 bg-gray-200 text-gray-500 rounded-full inline-block p-1 px-2 text-sm">
306+
{ account }
307+
</span>
308+
<CreatableSelect
309+
id='create-authors'
310+
styles={customStyles}
311+
placeholder="Input Authors"
312+
onChange={handleAuthorsChange}
313+
options={authorsInLocal ?? []}
314+
/>
315+
</div>
316+
<div className="pt-3 flex justify-start items-center text-gray-500">
317+
<span>Tags:</span>
318+
<CreatableSelect
319+
id='create-tags'
320+
styles={customStyles}
321+
isMulti
322+
placeholder="Input Post Tags"
323+
onChange={handleTagsChange}
324+
// @ts-ignore
325+
options={tagsInLocal ?? []}
326+
/>
327+
</div>
328+
</div>
329+
<Tiptap />
330+
</form>
331+
<Dialog as='div' open={isOpen} onClose={() => setIsOpen(false)}>
332+
<Dialog.Overlay />
333+
334+
<Dialog.Title>Deactivate account</Dialog.Title>
335+
<Dialog.Description>
336+
This will permanently deactivate your account
337+
</Dialog.Description>
338+
339+
<p>
340+
Are you sure you want to deactivate your account? All of your data will
341+
be permanently removed. This action cannot be undone.
342+
</p>
343+
344+
<button onClick={() => setIsOpen(false)}>Deactivate</button>
345+
<button onClick={() => setIsOpen(false)}>Cancel</button>
346+
</Dialog>
30347
</div>
31348
)
32349
})

components/Tiptap.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ import StarterKit from '@tiptap/starter-kit'
33
import { Paragraph } from "@tiptap/extension-paragraph"
44
import Placeholder from '@tiptap/extension-placeholder'
55
import { Heading } from '@tiptap/extension-heading'
6+
import { useLocalStorageValue } from "@react-hookz/web"
7+
import { CREATE_CACHE } from "../constants"
68

79
export const Tiptap = () => {
10+
const [, setCache] = useLocalStorageValue<any>(CREATE_CACHE)
811
const editor = useEditor({
12+
onUpdate: ({ editor : e }) => {
13+
const json = e.getJSON()
14+
setCache(json)
15+
},
916
extensions: [
1017
StarterKit,
1118
Paragraph.configure({

0 commit comments

Comments
 (0)