From 1abb8143db536c617b7c90c0bdd93c6f790ef8a1 Mon Sep 17 00:00:00 2001 From: eduairet Date: Tue, 6 Dec 2022 12:08:25 -0600 Subject: [PATCH 01/31] Endpoint created, testing pending --- frontend/pages/api/user-profile-data.js | 39 ++++++++++++++++++++++++ frontend/utils/test-user-profile-data.js | 34 +++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 frontend/pages/api/user-profile-data.js create mode 100644 frontend/utils/test-user-profile-data.js diff --git a/frontend/pages/api/user-profile-data.js b/frontend/pages/api/user-profile-data.js new file mode 100644 index 0000000..1fdbaaf --- /dev/null +++ b/frontend/pages/api/user-profile-data.js @@ -0,0 +1,39 @@ +import { Web3Storage, File } from 'web3.storage'; + +export default async function handler(req, res) { + if (req.method === 'POST') { + return await storeUserData(req, res); + } else { + return res + .status(405) + .json({ message: 'Method not allowed', success: false }); + } +} + +async function storeUserData(req, res) { + const body = req.body; + try { + const files = await makeFileObjects(body); + const cid = await storeFiles(files); + return res.status(200).json({ success: true, cid: cid }); + } catch (err) { + return res + .status(500) + .json({ error: 'Error creating user', success: false }); + } +} + +async function makeFileObjects(body) { + const buffer = Buffer.from(JSON.stringify(body)); + return new File([buffer], 'data.json'); +} + +function makeStorageClient() { + return new Web3Storage({ token: process.env.WEB3STORAGE_TOKEN }); +} + +async function storeFiles(files) { + const client = makeStorageClient(), + cid = await client.put(files); + return cid; +} diff --git a/frontend/utils/test-user-profile-data.js b/frontend/utils/test-user-profile-data.js new file mode 100644 index 0000000..ba6c066 --- /dev/null +++ b/frontend/utils/test-user-profile-data.js @@ -0,0 +1,34 @@ +console.log('Testing'); + +const body = { + email: 'test@ipfonts.xyz', + name: 'Immafont', + website: 'ipfonts.xyz', + bio: "I'm a test account", + links: [{ name: 'twitter', url: 'https://twitter.com/ipfonts' }], +}; + +async function testSubmit(body) { + try { + const response = await fetch('../api/store-font-data.js', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (response.status !== 200) { + console.log( + 'Oops! Something went wrong. Please refresh and try again.' + ); + } else { + console.log('User data successfully submitted!'); + let responseJSON = await response.json(); + console.log(responseJSON.cid); + } + } catch (error) { + console.log( + `Oops! Something went wrong. Please refresh and try again. Error ${error}` + ); + } +} + +testSubmit(); From ff0b0d8083f1972391da522003dd32ef861bc20f Mon Sep 17 00:00:00 2001 From: eduairet Date: Wed, 7 Dec 2022 18:33:56 -0600 Subject: [PATCH 02/31] Test profile successfully uploaded to IPFS --- frontend/pages/api/user-profile-data.js | 16 +++++------ frontend/pages/usertest.js | 35 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 frontend/pages/usertest.js diff --git a/frontend/pages/api/user-profile-data.js b/frontend/pages/api/user-profile-data.js index 1fdbaaf..3f7d09d 100644 --- a/frontend/pages/api/user-profile-data.js +++ b/frontend/pages/api/user-profile-data.js @@ -13,8 +13,8 @@ export default async function handler(req, res) { async function storeUserData(req, res) { const body = req.body; try { - const files = await makeFileObjects(body); - const cid = await storeFiles(files); + const file = await makeFileObject(body), + cid = await storeFile(file); return res.status(200).json({ success: true, cid: cid }); } catch (err) { return res @@ -23,17 +23,13 @@ async function storeUserData(req, res) { } } -async function makeFileObjects(body) { +async function makeFileObject(body) { const buffer = Buffer.from(JSON.stringify(body)); return new File([buffer], 'data.json'); } -function makeStorageClient() { - return new Web3Storage({ token: process.env.WEB3STORAGE_TOKEN }); -} - -async function storeFiles(files) { - const client = makeStorageClient(), - cid = await client.put(files); +async function storeFile(file) { + const client = new Web3Storage({ token: process.env.WEB3STORAGE_TOKEN }); + const cid = await client.put([file]); return cid; } diff --git a/frontend/pages/usertest.js b/frontend/pages/usertest.js new file mode 100644 index 0000000..d80439b --- /dev/null +++ b/frontend/pages/usertest.js @@ -0,0 +1,35 @@ +import Button from '../components/UI/Button'; + +export default function UserTest() { + async function handleCreateUser() { + const body = { + email: 'test@ipfonts.xyz', + name: 'Immafont', + website: 'ipfonts.xyz', + bio: "I'm a test account", + links: [{ name: 'twitter', url: 'https://twitter.com/ipfonts' }], + }; + try { + const response = await fetch('./api/user-profile-data', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (response.status !== 200) { + alert( + 'Oops! Something went wrong. Please refresh and try again.' + ); + } else { + let responseJSON = await response.json(); + alert( + `User data successfully submitted! Check it at https://${responseJSON.cid}.ipfs.w3s.link/data.json` + ); + } + } catch (error) { + alert( + `Oops! Something went wrong. Please refresh and try again. Error ${error}` + ); + } + } + return ; +} From d00ae68a2263cab7c31af764290596a213d4b54d Mon Sep 17 00:00:00 2001 From: eduairet Date: Thu, 8 Dec 2022 13:54:08 -0600 Subject: [PATCH 03/31] Create User form started --- frontend/components/UI/Form.js | 5 +++ frontend/components/UI/Input.js | 15 ++++++++ frontend/pages/usertest.js | 64 +++++++++++++++++++++++++++++---- frontend/styles/Form.module.css | 25 +++++++++++++ frontend/styles/globals.css | 4 +++ 5 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 frontend/components/UI/Form.js create mode 100644 frontend/components/UI/Input.js create mode 100644 frontend/styles/Form.module.css diff --git a/frontend/components/UI/Form.js b/frontend/components/UI/Form.js new file mode 100644 index 0000000..39c746d --- /dev/null +++ b/frontend/components/UI/Form.js @@ -0,0 +1,5 @@ +import classes from '../../styles/Form.module.css'; + +export default function Form(props) { + return
{props.children}
; +} diff --git a/frontend/components/UI/Input.js b/frontend/components/UI/Input.js new file mode 100644 index 0000000..fcd6939 --- /dev/null +++ b/frontend/components/UI/Input.js @@ -0,0 +1,15 @@ +import React, { useImperativeHandle, useRef } from 'react'; +import classes from '../../styles/Form.module.css'; + +export default function Input(props, ref) { + const inputRef = useRef(); + + return ( +
+ + +
+ ); +} diff --git a/frontend/pages/usertest.js b/frontend/pages/usertest.js index d80439b..99182a4 100644 --- a/frontend/pages/usertest.js +++ b/frontend/pages/usertest.js @@ -1,13 +1,60 @@ +import { useReducer } from 'react'; +// Components import Button from '../components/UI/Button'; +import Form from '../components/UI/Form'; +import Input from '../components/UI/Input'; + +const userFormReducer = (state, action) => { + console.log('Reducer'); +}; export default function UserTest() { + const [userFormState, dispatchUserForm] = useReducer(userFormReducer, { + email: '', + username: '', + website: '', + bio: '', + links: [], + }), + formInputChangeHandler = e => { + if (e.target.id === 'user-email') { + dispatchUserForm({ + type: 'EMAIL_INPUT', + val: e.target.value, + }); + } else if (e.target.id === 'username') { + dispatchUserForm({ + type: 'USERNAME_INPUT', + val: e.target.value, + }); + } else if (e.target.id === 'user-website') { + dispatchUserForm({ + type: 'WEBSITE_INPUT', + val: e.target.value, + }); + } else if (e.target.id === 'user-bio') { + dispatchUserForm({ + type: 'BIO_INPUT', + val: e.target.value, + }); + } else if (e.target.id === 'user-links') { + dispatchUserForm({ + type: 'LINKS_INPUT', + val: e.target.value, + }); + } + }, + validateFormInputHandler = e => { + dispatchUserForm({ type: 'INPUT_BLUR' }); + }; + async function handleCreateUser() { const body = { - email: 'test@ipfonts.xyz', - name: 'Immafont', - website: 'ipfonts.xyz', - bio: "I'm a test account", - links: [{ name: 'twitter', url: 'https://twitter.com/ipfonts' }], + email: userFormState.email, + name: userFormState.username, + website: userFormState.website, + bio: userFormState.bio, + links: userFormState.links, }; try { const response = await fetch('./api/user-profile-data', { @@ -31,5 +78,10 @@ export default function UserTest() { ); } } - return ; + return ( +
+ + +
+ ); } diff --git a/frontend/styles/Form.module.css b/frontend/styles/Form.module.css new file mode 100644 index 0000000..9405b8e --- /dev/null +++ b/frontend/styles/Form.module.css @@ -0,0 +1,25 @@ +.form { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + max-width: 350px; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.5rem; + color: var(--darkblue); +} + +.label { + text-align: left; + width: 100%; +} + +.input { + padding: 0.5rem; + color: var(--darkblue); +} diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css index 080d639..eb34fcb 100644 --- a/frontend/styles/globals.css +++ b/frontend/styles/globals.css @@ -84,6 +84,10 @@ strong { font-weight: 600; } +input { + padding: 0.25rem; +} + @media (hover) { a:hover { color: var(--darkblue); From 864ed2c1b39931e0556313efabdb8ed51d1bf975 Mon Sep 17 00:00:00 2001 From: eduairet Date: Sat, 10 Dec 2022 14:44:00 -0600 Subject: [PATCH 04/31] Form now uploads data to IPFS - Now you can fill the form in usertest.js UI and create a JSON in IPFS --- frontend/components/UI/Form.js | 6 +- frontend/components/UI/Input.js | 34 +++++-- frontend/pages/usertest.js | 158 ++++++++++++++++++++++++-------- frontend/styles/Form.module.css | 8 +- frontend/styles/globals.css | 17 ++++ 5 files changed, 178 insertions(+), 45 deletions(-) diff --git a/frontend/components/UI/Form.js b/frontend/components/UI/Form.js index 39c746d..c366630 100644 --- a/frontend/components/UI/Form.js +++ b/frontend/components/UI/Form.js @@ -1,5 +1,9 @@ import classes from '../../styles/Form.module.css'; export default function Form(props) { - return
{props.children}
; + return ( +
+ {props.children} +
+ ); } diff --git a/frontend/components/UI/Input.js b/frontend/components/UI/Input.js index fcd6939..d1b3552 100644 --- a/frontend/components/UI/Input.js +++ b/frontend/components/UI/Input.js @@ -1,15 +1,35 @@ -import React, { useImperativeHandle, useRef } from 'react'; +import React, { useRef, useImperativeHandle } from 'react'; import classes from '../../styles/Form.module.css'; -export default function Input(props, ref) { - const inputRef = useRef(); +const Input = React.forwardRef((props, ref) => { + const inputRef = useRef(), + activate = () => { + inputRef.current.focus(); + }; + useImperativeHandle(ref, () => { + return { focus: activate }; + }); return ( -
-
); }); diff --git a/frontend/pages/usertest.js b/frontend/pages/usertest.js index 7b24c8c..5470422 100644 --- a/frontend/pages/usertest.js +++ b/frontend/pages/usertest.js @@ -3,6 +3,8 @@ import { client as lensClient, createProfile, getProfileByAddress, + getProfileByHandle, + createSetProfileWithMetadata, } from '../clientApi'; import connectContract from '../utils/connectContract'; // Components @@ -10,6 +12,8 @@ import Form from '../components/UI/Form'; import Input from '../components/UI/Input'; const defaultFormValues = { + name: '', + nameIsValid: null, email: '', emailIsValid: null, username: '', @@ -18,8 +22,13 @@ const defaultFormValues = { websiteIsValid: null, bio: '', bioIsValid: null, - links: '', // Will change to an array later - linksAreValid: null, + links: { + twitter: { value: '', isValid: null }, + lenster: { value: '', isValid: null }, + instagram: { value: '', isValid: null }, + discord: { value: '', isValid: null }, + github: { value: '', isValid: null }, + }, }, userFormReducer = (state, action) => { const validators = { @@ -27,9 +36,36 @@ const defaultFormValues = { username: { grep: /[A-Za-z0-9-]+/, len: 2 }, website: { grep: /[A-Za-z0-9-]+\.[A-Za-z]+/, len: 5 }, bio: { min: 1, max: 120 }, - links: 1, + twitter: /https:\/\/twitter\.com\/[A-Za-z0-9]+/, + lenster: /https:\/\/lenster\.xyz\/u\/[A-Za-z0-9]+/, + instagram: /https:\/\/www.instagram.com\/[A-Za-z0-9]+/, + discord: /[A-Za-z0-9]+#\d{4}/, + github: /https:\/\/github\.com\/[A-Za-z0-9]+/, }; - switch (action.type) { + let currentLink, actionType; + if ( + [ + 'TWITTER_INPUT', + 'LENSTER_INPUT', + 'INSTAGRAM_INPUT', + 'DISCORD_INPUT', + 'GITHUB_INPUT', + ].includes(action.type) + ) { + currentLink = action.type.replace('_INPUT', '').toLowerCase(); + actionType = 'LINKS_INPUT'; + } else { + actionType = action.type; + } + switch (actionType) { + case 'NAME_INPUT': + return { + ...state, + name: action.val, + nameIsValid: + action.val.match(validators.username.grep) && + action.val.trim().length > validators.username.len, + }; case 'EMAIL_INPUT': return { ...state, @@ -63,8 +99,57 @@ const defaultFormValues = { case 'LINKS_INPUT': return { ...state, - links: action.val, - linksAreValid: action.val.trim().length >= validators.links, + links: { + ...state.links, + [currentLink]: { + value: action.val, + isValid: action.val.match(validators[currentLink]), + }, + }, + }; + case 'LENSTER_INPUT': + return { + ...state, + links: { + ...links, + lenster: { + value: action.val, + isValid: action.val.match(validators.lenster), + }, + }, + }; + case 'INSTAGRAM_INPUT': + return { + ...state, + links: { + ...links, + instagram: { + value: action.val, + isValid: action.val.match(validators.instagram), + }, + }, + }; + case 'DISCORD_INPUT': + return { + ...state, + links: { + ...links, + discord: { + value: action.val, + isValid: action.val.match(validators.discord), + }, + }, + }; + case 'GITHUB_INPUT': + return { + ...state, + links: { + ...links, + github: { + value: action.val, + isValid: action.val.match(validators.github), + }, + }, }; case 'INPUT_BLUR': return state; @@ -74,13 +159,20 @@ const defaultFormValues = { }; export default function UserTest(props) { - const [emailRef, usernameRef, websiteRef, bioRef, linksRef] = [ + const [nameRef, emailRef, usernameRef, websiteRef, bioRef] = [ useRef(), useRef(), useRef(), useRef(), useRef(), ], + links = [ + { name: 'Twitter', ref: useRef(), type: 'url' }, + { name: 'Lenster', ref: useRef(), type: 'url' }, + { name: 'Instagram', ref: useRef(), type: 'url' }, + { name: 'Discord', ref: useRef(), type: 'text' }, + { name: 'GitHub', ref: useRef(), type: 'url' }, + ], [userFormState, dispatchUserForm] = useReducer(userFormReducer, { ...defaultFormValues, }), @@ -88,7 +180,7 @@ export default function UserTest(props) { [lensHandle, setLensHandle] = useState(''), [txStatus, setTxStatus] = useState('DEFAULT'), formInputChangeHandler = e => { - const rawID = e.target.id.replace('user-', ''); + let rawID = e.target.id.replace('user-', ''); dispatchUserForm({ type: `${rawID.toUpperCase()}_INPUT`, val: e.target.value, @@ -135,11 +227,19 @@ export default function UserTest(props) { async function handleCreateUser(e) { e.preventDefault(); const body = { + name: userFormState.name, email: userFormState.email, - name: userFormState.username || lensHandle.replace('.test', ''), + username: userFormState.username || lensHandle.replace('.test', ''), website: userFormState.website, bio: userFormState.bio, - links: userFormState.links, + links: Object.fromEntries( + Object.entries(userFormState.links).map(([link, values]) => { + console.log(); + if (values.value) { + return [link, values.value]; + } + }) + ), }; // Deactivate inpute while waiting setTxStatus('WAIT'); @@ -171,9 +271,9 @@ export default function UserTest(props) { 'Oops! Something went wrong. Please refresh and try again.' ); } else { - const responseJSON = await response.json(); + const responseJSON = await response.json(), + cid = `https://${responseJSON.cid}.ipfs.w3s.link/data.json`; if (!hasIPFonts) { - const cid = `https://${responseJSON.cid}.ipfs.w3s.link/data.json`; const txn = await ipfontsContract.createUser( lensHandle, cid, @@ -186,6 +286,20 @@ export default function UserTest(props) { clearTimeout(reset); } else { alert('Modify user logic goes here!'); + if (lensHandle) { + const getProfile = await lensClient.query({ + query: getProfileByHandle, + variables: { handle: lensHandle }, + }); + console.log(getProfile.data.profile); + const setLensProfile = await lensClient.mutate({ + mutation: createSetProfileWithMetadata( + getProfile.data.profile.id, + cid + ), + }); + console.log(setLensProfile.data); + } resetForm(); } } @@ -205,9 +319,17 @@ export default function UserTest(props) { )} {props.connected && ( <> -
- {lensHandle ? `Welcome ${lensHandle}!` : 'Welcome!'} -
+
{lensHandle ? `Welcome ${lensHandle}` : 'Welcome'}
+ - + {links.map(link => ( + + ))} {txStatus === 'DEFAULT' ? ( Date: Wed, 14 Dec 2022 18:10:39 -0600 Subject: [PATCH 12/31] Page name changed New name: create-edit-user --- frontend/pages/{usertest.js => create-edit-user.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/pages/{usertest.js => create-edit-user.js} (100%) diff --git a/frontend/pages/usertest.js b/frontend/pages/create-edit-user.js similarity index 100% rename from frontend/pages/usertest.js rename to frontend/pages/create-edit-user.js From 5249db2f9062bd844e1bc7d9f291e074f9e6d88d Mon Sep 17 00:00:00 2001 From: eduairet Date: Fri, 16 Dec 2022 18:01:28 -0600 Subject: [PATCH 13/31] Upload metadata endpoint secured - Added the iron session api route to the metadata upload endpoint --- frontend/pages/api/user-profile-data.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/pages/api/user-profile-data.js b/frontend/pages/api/user-profile-data.js index 3f7d09d..7b6f34d 100644 --- a/frontend/pages/api/user-profile-data.js +++ b/frontend/pages/api/user-profile-data.js @@ -1,6 +1,7 @@ import { Web3Storage, File } from 'web3.storage'; +import { withIronSessionApiRoute } from 'iron-session/next'; -export default async function handler(req, res) { +async function handler(req, res) { if (req.method === 'POST') { return await storeUserData(req, res); } else { @@ -33,3 +34,5 @@ async function storeFile(file) { const cid = await client.put([file]); return cid; } + +export default withIronSessionApiRoute(handler, ironOptions); From 5f5d1ff7003632223c1708023f607190b2dec27c Mon Sep 17 00:00:00 2001 From: eduairet Date: Fri, 16 Dec 2022 20:43:31 -0600 Subject: [PATCH 14/31] Input as a class Inputs removed from globals and added to Form.module.css --- frontend/styles/Form.module.css | 19 +++++++++++++++++++ frontend/styles/globals.css | 24 ------------------------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/frontend/styles/Form.module.css b/frontend/styles/Form.module.css index afc0dd5..6bdb8e9 100644 --- a/frontend/styles/Form.module.css +++ b/frontend/styles/Form.module.css @@ -24,6 +24,25 @@ .input { padding: 0.5rem; color: var(--darkblue); + border: solid 2px var(--red); + border-radius: var(--global-radius); +} + +.input[type='submit'] { + margin-top: 1.5rem; + cursor: pointer; + background-color: var(--red); + color: var(--light); + border-radius: var(--global-radius); + border: 1px solid var(--red); + transition: all 0.5s ease; +} + +.input[type='submit']:disabled { + background-color: var(--light); + color: var(--darkblue); + border-color: var(--darkblue); + cursor: wait; } .invalid { diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css index 5850699..a74f3a6 100644 --- a/frontend/styles/globals.css +++ b/frontend/styles/globals.css @@ -84,30 +84,6 @@ strong { font-weight: 600; } -input, -textarea { - padding: 0.25rem; - border: 1px solid var(--red); - border-radius: var(--global-radius); -} - -input[type='submit'] { - margin-top: 1.5rem; - cursor: pointer; - background-color: var(--red); - color: var(--light); - border-radius: var(--global-radius); - border: 1px solid var(--red); - transition: all 0.5s ease; -} - -input[type='submit']:disabled { - background-color: var(--light); - color: var(--darkblue); - border-color: var(--darkblue); - cursor: wait; -} - @media (hover) { a:hover { color: var(--darkblue); From c204c27bf2110a6880dc04709f4adfe4c37b27b1 Mon Sep 17 00:00:00 2001 From: eduairet Date: Sat, 17 Dec 2022 00:21:08 -0600 Subject: [PATCH 15/31] Formik user form started --- frontend/package.json | 4 +- frontend/pages/create-edit-user.js | 2 +- frontend/pages/usertest.js | 117 +++++++++++++++++++++++++++++ pnpm-lock.yaml | 79 +++++++++++++++++-- 4 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 frontend/pages/usertest.js diff --git a/frontend/package.json b/frontend/package.json index 9085328..e34f3bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "apollo-client": "^2.6.10", "clsx": "^1.2.1", "ethers": "^5.7.2", + "formik": "^2.2.9", "graphql": "^15.8.0", "iron-session": "^6.3.1", "multer": "1.4.5-lts.1", @@ -33,7 +34,8 @@ "typescript": "^4.8.3", "utf8": "^3.0.0", "wagmi": "^0.8.9", - "web3.storage": "^4.4.0" + "web3.storage": "^4.4.0", + "yup": "^0.32.11" }, "devDependencies": { "autoprefixer": "^10.4.8", diff --git a/frontend/pages/create-edit-user.js b/frontend/pages/create-edit-user.js index 5470422..5783d9a 100644 --- a/frontend/pages/create-edit-user.js +++ b/frontend/pages/create-edit-user.js @@ -158,7 +158,7 @@ const defaultFormValues = { } }; -export default function UserTest(props) { +export default function CreateEditUser(props) { const [nameRef, emailRef, usernameRef, websiteRef, bioRef] = [ useRef(), useRef(), diff --git a/frontend/pages/usertest.js b/frontend/pages/usertest.js new file mode 100644 index 0000000..35b6321 --- /dev/null +++ b/frontend/pages/usertest.js @@ -0,0 +1,117 @@ +import { useFormik, Form, Field, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; +import classes from '../styles/Form.module.css'; + +const defaultFormValues = { + name: '', + email: '', + username: '', + website: '', + bio: '', + twitter: '', + lenster: '', + instagram: '', + discord: '', + github: '', +}; + +const FormikInput = props => { + return ( +
+ + {props.type !== 'text-area' ? ( + + ) : ( + + )} +
+ ); +}; + +const createEditUserInputs = [ + { id: 'name', type: 'text', label: 'Name' }, + { id: 'email', type: 'email', label: 'E-Mail' }, + { id: 'username', type: 'text', label: 'Username' }, + { id: 'website', type: 'url', label: 'Website' }, + { id: 'bio', type: 'text-area', label: 'Bio' }, + { id: 'twitter', type: 'url', label: 'Twitter' }, + { id: 'lenster', type: 'url', label: 'Lenster' }, + { id: 'instagram', type: 'url', label: 'Instagram' }, + { id: 'discord', type: 'text', label: 'Discord' }, + { id: 'github', type: 'url', label: 'GitHub' }, + ], + stringValid = [3, 'Must be ${this.len} characters or more'], + validateLinks = linkName => { + const greps = { + twitter: /https:\/\/twitter\.com\/[A-Za-z0-9]+/, + lenster: /https:\/\/lenster\.xyz\/u\/[A-Za-z0-9]+/, + instagram: /https:\/\/www.instagram.com\/[A-Za-z0-9]+/, + discord: /[A-Za-z0-9]+#\d{4}/, + github: /https:\/\/github\.com\/[A-Za-z0-9]+/, + }; + return Yup.string().matches(greps[linkName], { + message: `Invalid ${linkName} link`, + excludeEmptyString: true, + }); + }, + createEditUserValidationSchema = { + name: Yup.string().min(...stringValid), + email: Yup.string().email('Invalid email address'), + username: Yup.string().min(...stringValid), + website: Yup.string().url('Invalid website'), + bio: Yup.string().min(...stringValid), + twitter: validateLinks('twitter'), + lenster: validateLinks('lenster'), + instagram: validateLinks('instagram'), + discord: validateLinks('discord'), + github: validateLinks('github'), + }; + +export default function CreateEditUser() { + const formik = useFormik({ + initialValues: { + ...defaultFormValues, + }, + validationSchema: Yup.object({ ...createEditUserValidationSchema }), + onSubmit: values => { + alert(JSON.stringify(values, null, 2)); + }, + }); + + return ( +
+ {createEditUserInputs.map(field => ( + <> + + {formik.touched[field.id] && formik.errors[field.id] ? ( +
{formik.errors[id]}
+ ) : null} + + ))} + + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c49706..0d76fdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,7 @@ importers: eslint: 7.23.0 eslint-config-next: 12.2.5 ethers: ^5.7.2 + formik: ^2.2.9 graphql: ^15.8.0 iron-session: ^6.3.1 multer: 1.4.5-lts.1 @@ -108,6 +109,7 @@ importers: utf8: ^3.0.0 wagmi: ^0.8.9 web3.storage: ^4.4.0 + yup: ^0.32.11 dependencies: '@apollo/client': 3.7.1_iwx3feow7gzgnn2ozl3azqniue '@babel/core': 7.20.2 @@ -118,6 +120,7 @@ importers: apollo-client: 2.6.10_graphql@15.8.0 clsx: 1.2.1 ethers: 5.7.2 + formik: 2.2.9_react@18.1.0 graphql: 15.8.0 iron-session: 6.3.1_next@12.2.5 multer: 1.4.5-lts.1 @@ -131,6 +134,7 @@ importers: utf8: 3.0.0 wagmi: 0.8.9_yflvhnirygfio7wfwd2xnuvrsy web3.storage: 4.4.0_2zkwcmg3uyfbquk5hov4sasjd4 + yup: 0.32.11 devDependencies: autoprefixer: 10.4.13_postcss@8.4.19 eslint: 7.23.0 @@ -1604,7 +1608,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: core-js-pure: 3.26.1 - regenerator-runtime: 0.13.10 + regenerator-runtime: 0.13.11 dev: true /@babel/runtime/7.19.0: @@ -1617,14 +1621,14 @@ packages: resolution: {integrity: sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==} engines: {node: '>=6.9.0'} dependencies: - regenerator-runtime: 0.13.10 + regenerator-runtime: 0.13.11 + dev: true /@babel/runtime/7.20.6: resolution: {integrity: sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 - dev: false /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} @@ -3960,6 +3964,10 @@ packages: '@types/node': 18.11.10 dev: false + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} + dev: false + /@types/long/4.0.2: resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} dev: false @@ -5101,7 +5109,7 @@ packages: resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} engines: {node: '>=6.0'} dependencies: - '@babel/runtime': 7.20.1 + '@babel/runtime': 7.20.6 '@babel/runtime-corejs3': 7.20.1 dev: true @@ -6859,6 +6867,11 @@ packages: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} dev: false + /deepmerge/2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /deepmerge/3.3.0: resolution: {integrity: sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==} engines: {node: '>=0.10.0'} @@ -8589,6 +8602,21 @@ packages: mime-types: 2.1.35 dev: false + /formik/2.2.9_react@18.1.0: + resolution: {integrity: sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==} + peerDependencies: + react: '>=16.8.0' + dependencies: + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.21 + react: 18.1.0 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 1.14.1 + dev: false + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -10629,6 +10657,10 @@ packages: dependencies: p-locate: 5.0.0 + /lodash-es/4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.assign/4.2.0: resolution: {integrity: sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==} @@ -11033,7 +11065,7 @@ packages: /metro-runtime/0.72.3: resolution: {integrity: sha512-3MhvDKfxMg2u7dmTdpFOfdR71NgNNo4tzAyJumDVQKwnHYHN44f2QFZQqpPBEmqhWlojNeOxsqFsjYgeyMx6VA==} dependencies: - '@babel/runtime': 7.20.1 + '@babel/runtime': 7.20.6 react-refresh: 0.4.3 dev: false @@ -11535,6 +11567,10 @@ packages: /nano-json-stream-parser/0.1.2: resolution: {integrity: sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==} + /nanoclone/0.2.1: + resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} + dev: false + /nanoid/3.3.3: resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -12552,6 +12588,10 @@ packages: signal-exit: 3.0.7 dev: true + /property-expr/2.0.5: + resolution: {integrity: sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==} + dev: false + /protobufjs/6.11.3: resolution: {integrity: sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==} hasBin: true @@ -12772,6 +12812,10 @@ packages: scheduler: 0.22.0 dev: false + /react-fast-compare/2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13072,10 +13116,10 @@ packages: /regenerator-runtime/0.13.10: resolution: {integrity: sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==} + dev: false /regenerator-runtime/0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: false /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} @@ -13083,7 +13127,7 @@ packages: /regenerator-transform/0.15.0: resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} dependencies: - '@babel/runtime': 7.20.1 + '@babel/runtime': 7.20.6 dev: false /regex-not/1.0.2: @@ -14441,6 +14485,10 @@ packages: engines: {node: '>= 4.5.0'} dev: false + /tiny-warning/1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /title-case/2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} dependencies: @@ -14506,6 +14554,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /toposort/2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /touch/3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -16209,6 +16261,19 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /yup/0.32.11: + resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} + engines: {node: '>=10'} + dependencies: + '@babel/runtime': 7.20.6 + '@types/lodash': 4.14.191 + lodash: 4.17.21 + lodash-es: 4.17.21 + nanoclone: 0.2.1 + property-expr: 2.0.5 + toposort: 2.0.2 + dev: false + /zen-observable-ts/0.8.21: resolution: {integrity: sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==} dependencies: From 424ce38844f215e51b5e37603826988340fadb58 Mon Sep 17 00:00:00 2001 From: eduairet Date: Sat, 17 Dec 2022 16:37:58 -0600 Subject: [PATCH 16/31] Create profile - Now create profile works with variables - Missing ironOptions imported to endpoint --- frontend/clientApi/lens/createProfile.js | 12 +++--------- frontend/pages/api/user-profile-data.js | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/frontend/clientApi/lens/createProfile.js b/frontend/clientApi/lens/createProfile.js index c03c189..a446346 100644 --- a/frontend/clientApi/lens/createProfile.js +++ b/frontend/clientApi/lens/createProfile.js @@ -1,14 +1,8 @@ import { gql } from '@apollo/client'; -export const createProfile = handle => gql` - mutation CreateProfile { - createProfile( - request: { - handle: "${handle}" - profilePictureUri: null - followModule: { freeFollowModule: true } - } - ) { +export const createProfile = gql` + mutation CreateProfile($request: CreateProfileRequest!) { + createProfile(request: $request) { ... on RelayerResult { txHash } diff --git a/frontend/pages/api/user-profile-data.js b/frontend/pages/api/user-profile-data.js index 7b6f34d..48150b8 100644 --- a/frontend/pages/api/user-profile-data.js +++ b/frontend/pages/api/user-profile-data.js @@ -1,5 +1,6 @@ import { Web3Storage, File } from 'web3.storage'; import { withIronSessionApiRoute } from 'iron-session/next'; +import ironOptions from '../../config/ironOptions'; async function handler(req, res) { if (req.method === 'POST') { From 09f178c3576c21dbcd3dc2e598a852abefeb02db Mon Sep 17 00:00:00 2001 From: eduairet Date: Sat, 17 Dec 2022 22:02:53 -0600 Subject: [PATCH 17/31] Formik implemented - Formik library and Yup implemented - Default profile query when user creates profile - Queries and mutations now work with variables - Reducer form replaced by formik --- frontend/clientApi/index.js | 1 + .../clientApi/lens/createSetDefaultProfile.js | 32 ++ .../lens/createSetProfileWithMetadata.js | 58 +-- .../clientApi/lens/getProfileByAddress.js | 2 +- frontend/components/UI/Input.js | 44 +- frontend/pages/create-edit-user.js | 493 +++++++----------- frontend/pages/usertest.js | 117 ----- 7 files changed, 256 insertions(+), 491 deletions(-) create mode 100644 frontend/clientApi/lens/createSetDefaultProfile.js delete mode 100644 frontend/pages/usertest.js diff --git a/frontend/clientApi/index.js b/frontend/clientApi/index.js index c24cbac..d51fc9e 100644 --- a/frontend/clientApi/index.js +++ b/frontend/clientApi/index.js @@ -25,6 +25,7 @@ export const client = new ApolloClient({ export * from './lens/authenticate'; export * from './lens/challenge'; export * from './lens/createProfile'; +export * from './lens/createSetDefaultProfile'; export * from './lens/exploreProfiles'; export * from './lens/getDefaultProfile'; export * from './lens/getProfileByHandle'; diff --git a/frontend/clientApi/lens/createSetDefaultProfile.js b/frontend/clientApi/lens/createSetDefaultProfile.js new file mode 100644 index 0000000..fddef70 --- /dev/null +++ b/frontend/clientApi/lens/createSetDefaultProfile.js @@ -0,0 +1,32 @@ +import { gql } from '@apollo/client'; + +export const createSetDefaultProfile = gql` + mutation CreateSetDefaultProfileTypedData( + $request: CreateSetDefaultProfileRequest! + ) { + createSetDefaultProfileTypedData(request: $request) { + id + expiresAt + typedData { + types { + SetDefaultProfileWithSig { + name + type + } + } + domain { + name + chainId + version + verifyingContract + } + value { + nonce + deadline + wallet + profileId + } + } + } + } +`; diff --git a/frontend/clientApi/lens/createSetProfileWithMetadata.js b/frontend/clientApi/lens/createSetProfileWithMetadata.js index 303b628..74d376b 100644 --- a/frontend/clientApi/lens/createSetProfileWithMetadata.js +++ b/frontend/clientApi/lens/createSetProfileWithMetadata.js @@ -1,37 +1,35 @@ import { gql } from '@apollo/client'; -export const createSetProfileWithMetadata = (profileId, metadata) => { - return gql` - mutation CreateSetProfileMetadataTypedData { - createSetProfileMetadataTypedData( - request: { - profileId: "${profileId}", - metadata: "${metadata}" - } - ) { - id - expiresAt - typedData { - types { - SetProfileMetadataURIWithSig { - name - type - } - } - domain { +export const createSetProfileWithMetadata = gql` + mutation CreateSetProfileMetadataTypedData( + $profileId: ProfileId! + $metadata: Url! + ) { + createSetProfileMetadataTypedData( + request: { profileId: $profileId, metadata: $metadata } + ) { + id + expiresAt + typedData { + types { + SetProfileMetadataURIWithSig { name - chainId - version - verifyingContract - } - value { - nonce - deadline - profileId - metadata + type } } + domain { + name + chainId + version + verifyingContract + } + value { + nonce + deadline + profileId + metadata + } } } - `; -}; + } +`; diff --git a/frontend/clientApi/lens/getProfileByAddress.js b/frontend/clientApi/lens/getProfileByAddress.js index e528108..d28761c 100644 --- a/frontend/clientApi/lens/getProfileByAddress.js +++ b/frontend/clientApi/lens/getProfileByAddress.js @@ -4,7 +4,7 @@ export const getProfileByAddress = gql` query Profiles($owner: EthereumAddress!) { profiles(request: { ownedBy: [$owner] }) { items { - id, + id handle } } diff --git a/frontend/components/UI/Input.js b/frontend/components/UI/Input.js index 10781f0..68c9565 100644 --- a/frontend/components/UI/Input.js +++ b/frontend/components/UI/Input.js @@ -1,45 +1,31 @@ -import React, { useRef, useImperativeHandle } from 'react'; import classes from '../../styles/Form.module.css'; -const Input = React.forwardRef((props, ref) => { - const inputRef = useRef(), - activate = () => { - inputRef.current.focus(); - }; - useImperativeHandle(ref, () => { - return { focus: activate }; - }); - +export default function FormikInput(props) { return ( -
+
- {props.type === 'text-area' ? ( - + value={props.value} + /> ) : ( - + value={props.value} + > )}
); -}); - -Input.displayName = 'Input'; -export default Input; +} diff --git a/frontend/pages/create-edit-user.js b/frontend/pages/create-edit-user.js index 5783d9a..2dd3100 100644 --- a/frontend/pages/create-edit-user.js +++ b/frontend/pages/create-edit-user.js @@ -1,198 +1,80 @@ -import { useState, useEffect, useRef, useReducer } from 'react'; +import React, { useState, useEffect } from 'react'; import { client as lensClient, + getDefaultProfile, + createSetDefaultProfile, createProfile, - getProfileByAddress, getProfileByHandle, + getProfileByAddress, createSetProfileWithMetadata, } from '../clientApi'; import connectContract from '../utils/connectContract'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import classes from '../styles/Form.module.css'; // Components import Form from '../components/UI/Form'; import Input from '../components/UI/Input'; const defaultFormValues = { name: '', - nameIsValid: null, email: '', - emailIsValid: null, - username: '', - usernameIsValid: null, + handle: '', website: '', - websiteIsValid: null, bio: '', - bioIsValid: null, - links: { - twitter: { value: '', isValid: null }, - lenster: { value: '', isValid: null }, - instagram: { value: '', isValid: null }, - discord: { value: '', isValid: null }, - github: { value: '', isValid: null }, - }, + twitter: '', + lenster: '', + instagram: '', + discord: '', + github: '', }, - userFormReducer = (state, action) => { - const validators = { - email: /[^@]+@[A-Za-z0-9-]+\.[A-Za-z]+/, - username: { grep: /[A-Za-z0-9-]+/, len: 2 }, - website: { grep: /[A-Za-z0-9-]+\.[A-Za-z]+/, len: 5 }, - bio: { min: 1, max: 120 }, + createEditUserInputs = [ + { id: 'name', type: 'text', label: 'Name' }, + { id: 'email', type: 'email', label: 'E-Mail' }, + { id: 'handle', type: 'text', label: 'Handle' }, + { id: 'website', type: 'url', label: 'Website' }, + { id: 'bio', type: 'text-area', label: 'Bio' }, + { id: 'twitter', type: 'url', label: 'Twitter' }, + { id: 'lenster', type: 'url', label: 'Lenster' }, + { id: 'instagram', type: 'url', label: 'Instagram' }, + { id: 'discord', type: 'text', label: 'Discord' }, + { id: 'github', type: 'url', label: 'GitHub' }, + ], + minStringValid = [3, 'Must be 3 characters or more'], + validateLinks = linkName => { + const greps = { twitter: /https:\/\/twitter\.com\/[A-Za-z0-9]+/, lenster: /https:\/\/lenster\.xyz\/u\/[A-Za-z0-9]+/, instagram: /https:\/\/www.instagram.com\/[A-Za-z0-9]+/, discord: /[A-Za-z0-9]+#\d{4}/, github: /https:\/\/github\.com\/[A-Za-z0-9]+/, }; - let currentLink, actionType; - if ( - [ - 'TWITTER_INPUT', - 'LENSTER_INPUT', - 'INSTAGRAM_INPUT', - 'DISCORD_INPUT', - 'GITHUB_INPUT', - ].includes(action.type) - ) { - currentLink = action.type.replace('_INPUT', '').toLowerCase(); - actionType = 'LINKS_INPUT'; - } else { - actionType = action.type; - } - switch (actionType) { - case 'NAME_INPUT': - return { - ...state, - name: action.val, - nameIsValid: - action.val.match(validators.username.grep) && - action.val.trim().length > validators.username.len, - }; - case 'EMAIL_INPUT': - return { - ...state, - email: action.val, - emailIsValid: action.val.match(validators.email), - }; - case 'USERNAME_INPUT': - return { - ...state, - username: action.val, - usernameIsValid: - action.val.match(validators.username.grep) && - action.val.trim().length > validators.username.len, - }; - case 'WEBSITE_INPUT': - return { - ...state, - website: action.val, - websiteIsValid: - action.val.match(validators.website.grep) && - action.val.trim().length > validators.website.len, - }; - case 'BIO_INPUT': - return { - ...state, - bio: action.val, - bioIsValid: - action.val.trim().length >= validators.bio.min && - action.val.trim().length <= validators.bio.max, - }; - case 'LINKS_INPUT': - return { - ...state, - links: { - ...state.links, - [currentLink]: { - value: action.val, - isValid: action.val.match(validators[currentLink]), - }, - }, - }; - case 'LENSTER_INPUT': - return { - ...state, - links: { - ...links, - lenster: { - value: action.val, - isValid: action.val.match(validators.lenster), - }, - }, - }; - case 'INSTAGRAM_INPUT': - return { - ...state, - links: { - ...links, - instagram: { - value: action.val, - isValid: action.val.match(validators.instagram), - }, - }, - }; - case 'DISCORD_INPUT': - return { - ...state, - links: { - ...links, - discord: { - value: action.val, - isValid: action.val.match(validators.discord), - }, - }, - }; - case 'GITHUB_INPUT': - return { - ...state, - links: { - ...links, - github: { - value: action.val, - isValid: action.val.match(validators.github), - }, - }, - }; - case 'INPUT_BLUR': - return state; - case 'FORM_RESET': - return { ...defaultFormValues }; - } + return Yup.string().matches(greps[linkName], { + message: `Invalid ${linkName} ${ + linkName === 'discord' ? 'name' : 'link' + }`, + excludeEmptyString: true, + }); + }, + createEditUserValidationSchema = { + name: Yup.string().min(...minStringValid), + email: Yup.string().email('Invalid email address'), + handle: Yup.string().min(...minStringValid), + website: Yup.string().url('Invalid website'), + bio: Yup.string() + .min(...minStringValid) + .max(120, 'Must be less than 120 characters'), + twitter: validateLinks('twitter'), + lenster: validateLinks('lenster'), + instagram: validateLinks('instagram'), + discord: validateLinks('discord'), + github: validateLinks('github'), }; export default function CreateEditUser(props) { - const [nameRef, emailRef, usernameRef, websiteRef, bioRef] = [ - useRef(), - useRef(), - useRef(), - useRef(), - useRef(), - ], - links = [ - { name: 'Twitter', ref: useRef(), type: 'url' }, - { name: 'Lenster', ref: useRef(), type: 'url' }, - { name: 'Instagram', ref: useRef(), type: 'url' }, - { name: 'Discord', ref: useRef(), type: 'text' }, - { name: 'GitHub', ref: useRef(), type: 'url' }, - ], - [userFormState, dispatchUserForm] = useReducer(userFormReducer, { - ...defaultFormValues, - }), - [hasIPFonts, setHasIPFonts] = useState(false), - [lensHandle, setLensHandle] = useState(''), - [txStatus, setTxStatus] = useState('DEFAULT'), - formInputChangeHandler = e => { - let rawID = e.target.id.replace('user-', ''); - dispatchUserForm({ - type: `${rawID.toUpperCase()}_INPUT`, - val: e.target.value, - }); - }, - validateFormInputHandler = () => { - dispatchUserForm({ type: 'INPUT_BLUR' }); - }, - resetForm = () => { - dispatchUserForm({ type: 'FORM_RESET' }); - setTxStatus('DEFAULT'); - }; + const [hasIPFonts, setHasIPFonts] = useState(false), + [lensHandle, setLensHandle] = useState(null), + [waitForm, setWaitForm] = useState(false); // Check if the user has IPFonts and lens profile useEffect(() => { @@ -209,81 +91,115 @@ export default function CreateEditUser(props) { ); setHasIPFonts(Boolean(ipfontsProfile.createdAt.toNumber())); // Has Lens profile - const getLensUser = await lensClient.mutate({ - mutation: getProfileByAddress, - variables: { owner: props.address }, - }), - hasLens = Boolean(getLensUser.data.profiles.items.length); - setLensHandle( - hasLens ? getLensUser.data.profiles.items[0].handle : '' - ); + const getLensUser = await lensClient.query({ + query: getDefaultProfile, + variables: { request: { ethereumAddress: props.address } }, + }); + // Setting a default profile is not working for some reason + setLensHandle(getLensUser.data.defaultProfile?.handle); } catch (err) { - setHasIPFonts(''); - setLensHandle(''); + setHasIPFonts(''), setLensHandle(''); } })(); }, [props.address]); - async function handleCreateUser(e) { - e.preventDefault(); - const body = { - name: userFormState.name, - email: userFormState.email, - username: userFormState.username || lensHandle.replace('.test', ''), - website: userFormState.website, - bio: userFormState.bio, - links: Object.fromEntries( - Object.entries(userFormState.links).map(([link, values]) => { - console.log(); - if (values.value) { - return [link, values.value]; - } - }) - ), - }; - // Deactivate inpute while waiting - setTxStatus('WAIT'); - // Creates a test lens profile with the username as handle - if (!lensHandle) { + // Create/edit user function + async function handleCreateEditUser(body, resetFunction) { + setWaitForm(true); + // Creates a test lens profile + const getLensUser = await lensClient.query({ + query: getDefaultProfile, + variables: { request: { ethereumAddress: props.address } }, + }); + if (!getLensUser.data.defaultProfile?.handle) { try { const createLensProfile = await lensClient.mutate({ - mutation: createProfile(userFormState.username), + mutation: createProfile, + variables: { + request: { + handle: body.handle, + profilePictureUri: null, + followModule: { freeFollowModule: true }, + followNFTURI: null, + }, + }, }); - if (createLensProfile.data) - setLensHandle(userFormState.username); + if ( + createLensProfile.data.createProfile.reason === + 'HANDLE_TAKEN' + ) { + alert('Lens handle taken, change it and try again!'); + setWaitForm(false); + return; + } + if (createLensProfile.data.createProfile.txHash) { + const profileId = await lensClient.query({ + query: getProfileByAddress, + variables: { owner: props.address }, + }), + profiles = profileId.data.profiles.items, + setProfile = await lensClient.mutate({ + mutation: createSetDefaultProfile, + variables: { + request: { + profileId: profiles[profiles.length - 1].id, + }, + }, + }); + if ( + setProfile.data.createSetDefaultProfileTypedData + .typedData.value.profileId + ) { + setLensHandle(`${body.handle}.test`); + resetFunction(); + } + } } catch (err) { alert( `There was an error creating your lens test profile ${err}` ); + resetFunction(); } - resetForm(); } try { - const ipfontsContract = await connectContract(); + const ipfontsContract = await connectContract(), + ipfontsBody = { + email: body.email, + name: body.name, + website: body.website, + bio: body.bio, + links: [ + { name: 'twitter', url: body.twitter }, + { name: 'lenster', url: body.lenster }, + { name: 'instagram', url: body.instagram }, + { name: 'discord', user: body.discord }, + { name: 'github', url: body.github }, + ], + }; + console.log(ipfontsBody); if (ipfontsContract) { const response = await fetch('./api/user-profile-data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), + body: JSON.stringify(ipfontsBody), }); if (response.status !== 200) { alert( 'Oops! Something went wrong. Please refresh and try again.' ); + resetFunction(); } else { const responseJSON = await response.json(), - cid = `https://${responseJSON.cid}.ipfs.w3s.link/data.json`; + cidURL = `https://${responseJSON.cid}.ipfs.w3s.link/data.json`; if (!hasIPFonts) { const txn = await ipfontsContract.createUser( lensHandle, - cid, + responseJSON.cid, Date.now(), { gasLimit: 900000 } ); const wait = await txn.wait(); - setTxStatus(!wait && 'SENT'); - const reset = setTimeout(resetForm, 2000); - clearTimeout(reset); + resetFunction(); } else { alert('Modify user logic goes here!'); if (lensHandle) { @@ -293,14 +209,15 @@ export default function CreateEditUser(props) { }); console.log(getProfile.data.profile); const setLensProfile = await lensClient.mutate({ - mutation: createSetProfileWithMetadata( - getProfile.data.profile.id, - cid - ), + mutation: createSetProfileWithMetadata, + variables: { + profileId: getProfile.data.profile.id, + metadata: cidURL, + }, }); console.log(setLensProfile.data); } - resetForm(); + resetFunction(); } } } else { @@ -308,115 +225,63 @@ export default function CreateEditUser(props) { } } catch (error) { alert(`Oops! Something went wrong. Please refresh and try again.`); - resetForm(); + resetFunction(); } } + // Formik structure + const formik = useFormik({ + initialValues: { + ...defaultFormValues, + }, + validationSchema: Yup.object({ ...createEditUserValidationSchema }), + onSubmit: (values, { resetForm }) => { + const setReset = () => { + setWaitForm(false); + resetForm(); + }; + handleCreateEditUser(values, setReset); + }, + }); + return ( -
+ {!props.connected && (
Connect your wallet to create or edit your user
)} {props.connected && ( <>
{lensHandle ? `Welcome ${lensHandle}` : 'Welcome'}
- - - {lensHandle ? ( - '' - ) : ( - - )} - - { + if (!Boolean(lensHandle && field.id === 'handle')) { + return ( + + + {formik.touched[field.id] && + formik.errors[field.id] ? ( +
+ {formik.errors[field.id]} +
+ ) : null} +
+ ); + } + })} + - {links.map(link => ( - - ))} - {txStatus === 'DEFAULT' ? ( - - ) : txStatus === 'WAIT' ? ( - - ) : txStatus === 'SENT' ? ( - - ) : ( - '' - )} )}
diff --git a/frontend/pages/usertest.js b/frontend/pages/usertest.js deleted file mode 100644 index 35b6321..0000000 --- a/frontend/pages/usertest.js +++ /dev/null @@ -1,117 +0,0 @@ -import { useFormik, Form, Field, ErrorMessage } from 'formik'; -import * as Yup from 'yup'; -import classes from '../styles/Form.module.css'; - -const defaultFormValues = { - name: '', - email: '', - username: '', - website: '', - bio: '', - twitter: '', - lenster: '', - instagram: '', - discord: '', - github: '', -}; - -const FormikInput = props => { - return ( -
- - {props.type !== 'text-area' ? ( - - ) : ( - - )} -
- ); -}; - -const createEditUserInputs = [ - { id: 'name', type: 'text', label: 'Name' }, - { id: 'email', type: 'email', label: 'E-Mail' }, - { id: 'username', type: 'text', label: 'Username' }, - { id: 'website', type: 'url', label: 'Website' }, - { id: 'bio', type: 'text-area', label: 'Bio' }, - { id: 'twitter', type: 'url', label: 'Twitter' }, - { id: 'lenster', type: 'url', label: 'Lenster' }, - { id: 'instagram', type: 'url', label: 'Instagram' }, - { id: 'discord', type: 'text', label: 'Discord' }, - { id: 'github', type: 'url', label: 'GitHub' }, - ], - stringValid = [3, 'Must be ${this.len} characters or more'], - validateLinks = linkName => { - const greps = { - twitter: /https:\/\/twitter\.com\/[A-Za-z0-9]+/, - lenster: /https:\/\/lenster\.xyz\/u\/[A-Za-z0-9]+/, - instagram: /https:\/\/www.instagram.com\/[A-Za-z0-9]+/, - discord: /[A-Za-z0-9]+#\d{4}/, - github: /https:\/\/github\.com\/[A-Za-z0-9]+/, - }; - return Yup.string().matches(greps[linkName], { - message: `Invalid ${linkName} link`, - excludeEmptyString: true, - }); - }, - createEditUserValidationSchema = { - name: Yup.string().min(...stringValid), - email: Yup.string().email('Invalid email address'), - username: Yup.string().min(...stringValid), - website: Yup.string().url('Invalid website'), - bio: Yup.string().min(...stringValid), - twitter: validateLinks('twitter'), - lenster: validateLinks('lenster'), - instagram: validateLinks('instagram'), - discord: validateLinks('discord'), - github: validateLinks('github'), - }; - -export default function CreateEditUser() { - const formik = useFormik({ - initialValues: { - ...defaultFormValues, - }, - validationSchema: Yup.object({ ...createEditUserValidationSchema }), - onSubmit: values => { - alert(JSON.stringify(values, null, 2)); - }, - }); - - return ( -
- {createEditUserInputs.map(field => ( - <> - - {formik.touched[field.id] && formik.errors[field.id] ? ( -
{formik.errors[id]}
- ) : null} - - ))} - - - ); -} From a258db057473c4e00762fd45b06fc0f3e2a224ab Mon Sep 17 00:00:00 2001 From: eduairet Date: Sat, 17 Dec 2022 22:08:08 -0600 Subject: [PATCH 18/31] Logs cleaned, input name changed --- frontend/components/UI/Input.js | 2 +- frontend/pages/create-edit-user.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/components/UI/Input.js b/frontend/components/UI/Input.js index 68c9565..2375629 100644 --- a/frontend/components/UI/Input.js +++ b/frontend/components/UI/Input.js @@ -1,6 +1,6 @@ import classes from '../../styles/Form.module.css'; -export default function FormikInput(props) { +export default function Input(props) { return (