From 82e13329de835b5b911d1d814faab2450a4a974e Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:45:01 +0100 Subject: [PATCH 01/20] feat: update UX requests --- client/src/pages/browse-requests.tsx | 123 ++++++++++--- client/src/pages/home.tsx | 105 +++++++++-- client/src/pages/request-solution.tsx | 242 +++++++++++++++----------- client/src/root.tsx | 18 +- client/src/styles/components.tsx | 41 ++++- client/src/styles/main.css | 16 ++ 6 files changed, 392 insertions(+), 153 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 685c1f0f..fa989728 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -34,6 +34,7 @@ import { TextField, Fab, Tooltip, + useTheme, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SendIcon from '@mui/icons-material/Send'; @@ -44,6 +45,10 @@ import { motion } from 'framer-motion'; import { StyledMuiButton } from '@/styles/components'; import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded'; import useScroll from '@/hooks/useScroll'; +import useWindowDimensions from '@/hooks/useWindowDimensions'; +import LibraryAddRoundedIcon from '@mui/icons-material/LibraryAddRounded'; +import { InfoRounded } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; interface IrysTx { id: string; @@ -263,8 +268,14 @@ const RequestElement = ({ request }: { request: RequestData }) => { - - + + {request.description} { className='flex flex-col gap-1' style={{ fontWeight: 600, padding: '10px 15px', fontSize: '18px', marginTop: '5px' }} > - {request.title} + {request.title.substring(0, 200) + (request.title.length > 200 ? ' (...)' : '')} {`${new Date(Number(request.timestamp) * 1000).toLocaleString()} by `} @@ -379,10 +390,13 @@ const RequestElement = ({ request }: { request: RequestData }) => { sx={{ WebkitLineClamp: 3, paddingBottom: '20px', + textWrap: 'wrap', + wordBreak: 'break-word', fontWeight: 400, }} > - {request.description} + {request.description.substring(0, 500) + + (request.description.length > 500 ? ' (...)' : '')} { ); }; +const MakeRequestMessage = ({ smallScreen }: { smallScreen: boolean }) => { + const navigate = useNavigate(); + const openRequestsRoute = useCallback(() => navigate('/request'), [navigate]); + + return ( +
+
+ + + Are you looking for custom made, tailored solutions for your own projects? +
+ Create your own request listing, define your budget and quickly get amazing solutions + tailored for you by the trusted FairAI community members. +
+ + + + Create a request + +
+
+ ); +}; + const BrowseRequests = () => { const [requests, setRequests] = useState([]); - const [ filtering, setFiltering ] = useState(false); + const [filtering, setFiltering] = useState(false); const ref = useRef(null); const bottomRef = useRef(null); const { isAtBottom, isScrolled } = useScroll(ref); + const theme = useTheme(); + const { width } = useWindowDimensions(); + const [isSmallScreen, setIsSmallScreen] = useState(false); + + useEffect(() => { + const md = theme.breakpoints.values.md; + setIsSmallScreen(width < md); + }, [width, theme, setIsSmallScreen]); const { data, loading } = useQuery(irysQuery, { variables: { @@ -461,7 +531,7 @@ const BrowseRequests = () => { if (bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: 'smooth' }); } - }, [ bottomRef ]); + }, [bottomRef]); useEffect(() => { // remove scroll from main element @@ -479,17 +549,19 @@ const BrowseRequests = () => { }, []); // run only on first load return ( - <> -
- - +
-
+
+ + + + Go Back + +
{ /> Current Feature Requests
- - - - Go Back - - +
@@ -514,6 +581,8 @@ const BrowseRequests = () => { )} + {!isLoading && } + {requests.length === 0 && !isLoading && ( @@ -541,19 +610,21 @@ const BrowseRequests = () => { )} - +
- + - +
); }; diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index f3d575ea..c9ec714a 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -20,7 +20,15 @@ import { Box, Container, Grid, InputBase, Typography, useTheme } from '@mui/mate import '@/styles/ui.css'; import useSolutions from '@/hooks/useSolutions'; import LoadingCard from '@/components/loading-card'; -import { ChangeEvent, RefObject, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { + ChangeEvent, + RefObject, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import useOperators from '@/hooks/useOperators'; import { findTag, genLoadingArray } from '@/utils/common'; import Solution from '@/components/solution'; @@ -38,8 +46,15 @@ import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; import useWindowDimensions from '@/hooks/useWindowDimensions'; import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded'; +import { InfoRounded } from '@mui/icons-material'; -const WarningMessage = ({ smallScreen, containerRef }: { smallScreen: boolean, containerRef: RefObject }) => { +const WarningMessage = ({ + smallScreen, + containerRef, +}: { + smallScreen: boolean; + containerRef: RefObject; +}) => { const [showWarning, setShowWarning] = useState(false); const { currentAddress, usdcBalance } = useContext(EVMWalletContext); const navigate = useNavigate(); @@ -66,7 +81,7 @@ const WarningMessage = ({ smallScreen, containerRef }: { smallScreen: boolean, c } else { container.style.paddingBottom = '86px'; } - }, [ containerRef, localStorage, currentAddress, showWarning, usdcBalance, MIN_U_BALANCE ]); + }, [containerRef, localStorage, currentAddress, showWarning, usdcBalance, MIN_U_BALANCE]); if (!localStorage.getItem('evmProvider') && !currentAddress) { return ( @@ -169,6 +184,53 @@ const WarningMessage = ({ smallScreen, containerRef }: { smallScreen: boolean, c } }; +const B2bMessage = ({ smallScreen }: { smallScreen: boolean }) => { + const navigate = useNavigate(); + const openRequestsRoute = useCallback(() => navigate('/browse'), [navigate]); + + return ( +
+ + + Are you a business or company looking for custom made, tailored solutions for your own + projects? +
+ Make a request, define your budget and quickly get amazing solutions tailored for you by the + trusted FairAI community members. +
+ + + + Go to requests + +
+ ); +}; + export default function Home() { const target = useRef(null); const { loading, txs, error, refetch } = useSolutions(target); @@ -205,7 +267,6 @@ export default function Home() { setFilteredTxs(txs); }, [txs]); - useEffect(() => { // remove scroll from main element const mainEl = document.getElementById('main'); @@ -228,7 +289,7 @@ export default function Home() { animate={{ y: 0, opacity: 1, transition: { delay: 0.2, duration: 0.4 } }} className='flex justify-center w-full' > - +
-
+
Choose a solution and start creating @@ -316,17 +377,25 @@ export default function Home() { )} - {loading && - {loadingTiles.map((el) => ( - - - - ))} - } + {loading && ( + + {loadingTiles.map((el) => ( + + + + ))} + + )} + + {!error && !loading && ( +
+ +
+ )} {!error && !loading && ( ; + return ( + + ); }; const RequestSolution = () => { @@ -66,7 +105,7 @@ const RequestSolution = () => { defaultValues: { title: '', description: '', - keywords: [], + keywords: ['asd'], }, }); const { field: title } = useController({ control, name: 'title', rules: { required: true } }); @@ -141,47 +180,34 @@ const RequestSolution = () => { } return ( - - - - - - - -
- - Tell us about your needs +
+
+
+
+ + + Create a request: tell the community what you need. + +
+ +
+ + + +
- - - Please provide a short sentence that describes your problem +
+ + 1. Provide a short title that describes your problem or idea + + + This will be shown as the title of your request. { onBlur={title.onBlur} fullWidth variant='outlined' + placeholder='Write a title or subject...' sx={{ backgroundColor: 'white', overflow: 'hidden', borderRadius: '8px' }} /> - - - - Please provide a detailed description of your problem +
+
+ + 2. Provide a detailed description + + + Explain what you need, the context, the problem and what are your initial suggestions or + requisites. You should provide as much information as possible so that whoever reads + this request can get a straightfoward idea of how we can build a custom solution for + you. Remember that everything you write here will be public and visible to everyone. - - - - Add a few keywords +
+
+ + 3. Add keywords (categories) that best suit your request - - {keywords.map((keyword) => ( - - ))} - - ), - }} - /> - - - + + Add at least two or three keywords that best fit your request. It will make it easier + for others to understand what your need and find your request faster. Some members of + the community like to focus on certain categories or keywords. Add up to 6 keywords. + + +
+ {keywords.map((keyword) => ( + + ))} +
+ +
+ } + /> + + Add +
+
+ +
+ + 5. What is your initial budget for this request? + + + Providing an initially planned budget for your request will convince more developers to + try and accept your idea, as they see this budget as their reward for successfuly + completing your request. You can always talk and debate this budget with whoever accepts + your request. + +
+ +
+
+ +
+ Cancel @@ -265,11 +309,11 @@ const RequestSolution = () => { disabled={!formState.isDirty || !formState.isValid || formState.isSubmitted} > - Save and Submit + Submit Request - - - +
+
+
); }; diff --git a/client/src/root.tsx b/client/src/root.tsx index 0339a17d..39532e83 100644 --- a/client/src/root.tsx +++ b/client/src/root.tsx @@ -19,7 +19,7 @@ import { ApolloProvider } from '@apollo/client'; import { CssBaseline } from '@mui/material'; import { SnackbarProvider } from 'notistack'; -import { Outlet, useLocation } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import Layout from './components/layout'; import { client } from './utils/apollo'; import { AppThemeProvider } from './context/theme'; @@ -60,15 +60,15 @@ const BaseRoot = ({ children }: { children: ReactElement }) => { }; export const Root = () => { - const { pathname } = useLocation(); + // const { pathname } = useLocation(); - if (pathname === '/request') { - return ( - - - - ); - } + // if (pathname === '/request') { + // return ( + // + // + // + // ); + // } return ( diff --git a/client/src/styles/components.tsx b/client/src/styles/components.tsx index b718fb17..e0573700 100644 --- a/client/src/styles/components.tsx +++ b/client/src/styles/components.tsx @@ -1,4 +1,4 @@ -import { ButtonProps, styled } from '@mui/material'; +import { ButtonProps, Slider, styled } from '@mui/material'; import { MaterialDesignContent } from 'notistack'; export const LoadingContainer = styled('div')(({ theme }) => ({ @@ -230,3 +230,42 @@ export const StyledMuiButton = styled('button')(({ theme }) => ({ pointerEvents: 'none', }, })); + +export const PrettoSlider = styled(Slider)({ + color: '#52af77', + height: 8, + '& .MuiSlider-track': { + border: 'none', + }, + '& .MuiSlider-thumb': { + height: 24, + width: 24, + backgroundColor: '#fff', + border: '2px solid currentColor', + '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': { + boxShadow: 'inherit', + }, + '&::before': { + display: 'none', + }, + }, + '& .MuiSlider-valueLabel': { + lineHeight: 1.2, + fontSize: 12, + background: 'unset', + padding: 0, + width: 32, + height: 32, + borderRadius: '50% 50% 50% 0', + backgroundColor: '#52af77', + transformOrigin: 'bottom left', + transform: 'translate(50%, -100%) rotate(-45deg) scale(0)', + '&::before': { display: 'none' }, + '&.MuiSlider-valueLabelOpen': { + transform: 'translate(50%, -100%) rotate(-45deg) scale(1)', + }, + '& > *': { + transform: 'rotate(45deg)', + }, + }, +}); diff --git a/client/src/styles/main.css b/client/src/styles/main.css index 527db0b5..7a961d30 100644 --- a/client/src/styles/main.css +++ b/client/src/styles/main.css @@ -164,3 +164,19 @@ html { background-position: 86% 0%; } } + +@keyframes fadeInSlideDown { + 0% { + opacity: 0; + transform: translateY(-40px); + } + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +.animate-slide-down { + opacity: 0; + animation: 0.3s fadeInSlideDown ease-out forwards; +} From c32621aa8bcac64157a591e3e2d69908d31197f3 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:19:20 +0000 Subject: [PATCH 02/20] feat: requests - add budgets and suggestions --- client/src/pages/browse-requests.tsx | 264 ++++++++++++++++++++++---- client/src/pages/request-solution.tsx | 98 ++++++++-- 2 files changed, 308 insertions(+), 54 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index fa989728..294c8477 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -import DebounceIconButton from '@/components/debounce-icon-button'; import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; import { gql, useQuery } from '@apollo/client'; import Close from '@mui/icons-material/Close'; @@ -35,6 +34,11 @@ import { Fab, Tooltip, useTheme, + FormGroup, + FormControlLabel, + Checkbox, + FormControl, + MenuItem, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SendIcon from '@mui/icons-material/Send'; @@ -47,8 +51,17 @@ import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRound import useScroll from '@/hooks/useScroll'; import useWindowDimensions from '@/hooks/useWindowDimensions'; import LibraryAddRoundedIcon from '@mui/icons-material/LibraryAddRounded'; -import { InfoRounded } from '@mui/icons-material'; +import { + AddRounded, + ChatBubbleRounded, + CheckCircleRounded, + CloseRounded, + InfoRounded, +} from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; +import { NumericFormat } from 'react-number-format'; +import { DateField, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; interface IrysTx { id: string; @@ -132,11 +145,16 @@ const RequestElement = ({ request }: { request: RequestData }) => { const [open, setOpen] = useState(false); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); + const [showAddComment, setShowAddComment] = useState(false); + const [showAcceptAsDev, setShowAcceptAsDev] = useState(false); const { currentAddress } = useContext(EVMWalletContext); const handleOpen = useCallback(() => setOpen(true), [setOpen]); const handleClose = useCallback(() => setOpen(false), [setOpen]); + const handleShowNewComment = () => setShowAddComment(!showAddComment); + const handleShowAcceptAsDev = () => setShowAcceptAsDev(!showAcceptAsDev); + const { data: commentsData } = useQuery(irysQuery, { variables: { tags: [ @@ -290,10 +308,30 @@ const RequestElement = ({ request }: { request: RequestData }) => { label={keyword} variant='filled' color='primary' - className='font-bold saturate-50' + className='font-bold saturate-50 brightness-105' /> ))} +
+ + Request details + +
+
+ Planned budget: US$ 5,000.00 +
+
+ Payment / Deliveries: Monthly +
+
+ Date target: 02/28/2025 +
+
+ Time budget: 4 months +
+
+
+
{comments.length} {comments.length === 1 ? ' comment' : 'comments'}
@@ -326,38 +364,172 @@ const RequestElement = ({ request }: { request: RequestData }) => { animate={{ opacity: 1, y: 0, transition: { delay: 0.1 } }} >
- - - - ), - }} - /> + {!showAddComment && !showAcceptAsDev && ( +
+ + + + Add a comment + + + + + + + Accept and develop this request + + +
+ )} + + {showAddComment && ( +
+ + + + + + Add + +
+ )} + + {showAcceptAsDev && ( +
+ + Assign yourself as a + developer for this request + + + You are about to suggest yourself as a developer of this request. +
+ Everything you fill here will be publicly visible in this request! +
+ You can accept everything as is, or suggest a different budget, + different target date, time needed or a different payment plan. +
+ The request creator will be able to counter your offer with a different + one, or accept it. +
You can also leave any additional info or comments in the comment + box, as needed. +
+ + You suggestion / proposal + +
+ + } + label='I accept the request as is and want to start right away.' + /> + +
+ +
+ + + + Daily deliveries and payments + Weekly deliveries and payments + Monthly deliveries and payments + Yearly deliveries and payments + All at once, right at the start + All at once, when project ends + + + +
+ Time budget / date target suggestion + +
+ + + + or +
+ + + Day(s) + Week(s) + Month(s) + Year(s) + +
+
+
+
+ + Ways to contact you + +
+ + @ }} + /> +
+ + Anything else? + + + +
+ + Cancel + + + Submit + +
+
+ )}
)} @@ -405,16 +577,24 @@ const RequestElement = ({ request }: { request: RequestData }) => { gap={'5px'} flexWrap={'wrap'} > - - {comments.length} {comments.length === 1 ? ' comment' : ' comments'} - +
+ + +
{request.keywords.map((keyword) => ( ))} @@ -453,8 +633,8 @@ const MakeRequestMessage = ({ smallScreen }: { smallScreen: boolean }) => { Are you looking for custom made, tailored solutions for your own projects?
- Create your own request listing, define your budget and quickly get amazing solutions - tailored for you by the trusted FairAI community members. + Create your request listing, define your budget and quickly get amazing solutions tailored + for you by the trusted FairAI community members. { - Go Back + Back
diff --git a/client/src/pages/request-solution.tsx b/client/src/pages/request-solution.tsx index e3c72a42..2285b40d 100644 --- a/client/src/pages/request-solution.tsx +++ b/client/src/pages/request-solution.tsx @@ -20,12 +20,24 @@ import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; import { StyledMuiButton } from '@/styles/components'; import { postOnArweave } from '@fairai/evm-sdk'; import Close from '@mui/icons-material/Close'; -import { Autocomplete, Box, Button, Chip, TextField, Typography } from '@mui/material'; +import { + Autocomplete, + Box, + Button, + Chip, + FormControl, + MenuItem, + TextField, + Typography, +} from '@mui/material'; import { useSnackbar } from 'notistack'; import { useCallback, useState } from 'react'; import { UseFormSetValue, useController, useForm, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; +import { NumericFormat } from 'react-number-format'; +import { DateField, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; const defaultKeywordsList = [ 'AI', @@ -206,7 +218,7 @@ const RequestSolution = () => { 1. Provide a short title that describes your problem or idea - + This will be shown as the title of your request. { 2. Provide a detailed description - + Explain what you need, the context, the problem and what are your initial suggestions or requisites. You should provide as much information as possible so that whoever reads this request can get a straightfoward idea of how we can build a custom solution for @@ -247,10 +259,10 @@ const RequestSolution = () => { 3. Add keywords (categories) that best suit your request - + Add at least two or three keywords that best fit your request. It will make it easier - for others to understand what your need and find your request faster. Some members of - the community like to focus on certain categories or keywords. Add up to 6 keywords. + for developers to understand what your need and find your request faster. Some + developers like to focus on certain categories. Add up to 6 keywords.
@@ -287,18 +299,80 @@ const RequestSolution = () => { 5. What is your initial budget for this request? - + Providing an initially planned budget for your request will convince more developers to try and accept your idea, as they see this budget as their reward for successfuly - completing your request. You can always talk and debate this budget with whoever accepts - your request. + completing your request. You can always talk and debate this budget with the developers. -
- +
+
-
+
+ + 6. What will be your prefered payment plan to the developers? + + + Paying a defined portion of your total budget for each successful partial feature + delivery by the developers will make it feel safer for both sides to invest in your + request. You get deliveries in a timely manner, and the developers, their payment + portions. + +
+ + + Daily deliveries and payments + Weekly deliveries and payments + Monthly deliveries and payments + Yearly deliveries and payments + All at once, right at the start + All at once, when project ends + + +
+
+ +
+ + 7. What is your initial date target for this request? + + + Tell us when you would most likely want the final portion of this request to be + delivered. Take in consideration the effort needed to achieve your needs for this + request. This is only a theoretical target, and can always be further discussed with the + developers. + +
+
+ + + +
+
or
+
+ + + Day(s) + Week(s) + Month(s) + Year(s) + +
+
+
+ +
Cancel From 079a653039ddb09793b30f8129088a32a9caf78f Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:30:21 +0000 Subject: [PATCH 03/20] feat: improved ux --- client/src/pages/browse-requests.tsx | 97 +++++++++++++++++++++++----- client/src/styles/ui.css | 6 -- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 294c8477..98e5240b 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -57,6 +57,7 @@ import { CheckCircleRounded, CloseRounded, InfoRounded, + StarRounded, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import { NumericFormat } from 'react-number-format'; @@ -114,30 +115,93 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque }, [comment]); return ( -
- + <> +
+ +
+ {' '} + + {'Commented by '} + + + {comment.owner.slice(0, 6)}...{comment.owner.slice(-4)} + + + {` on ${new Date(Number(comment.timestamp) * 1000).toLocaleString()}`} + + {comment.owner === request.owner && ( + +
+ Request Creator +
+
+ )} +
+
{comment.content}
+
+
+ +
- {' '} - - {'Commented by '} + + + {'Application / suggestion by '} {comment.owner.slice(0, 6)}...{comment.owner.slice(-4)} {` on ${new Date(Number(comment.timestamp) * 1000).toLocaleString()}`} - + {comment.owner === request.owner && ( -
- Request Creator +
+ Application / Suggestion
)}
-
{comment.content}
- -
+
+
+ Budget suggestion: US$ 6,000.00 +
+
+ Time suggestion: 6 month(s) +
+
+ Payment / Deliveries: All at once, right at the start +
+
+
+ {' '} + This would be a very difficult project, so I would like to make this suggestion before + accepting it. If we reach an agreement, I can start right away. +
+
+
+ X / Twitter: {' '} + + @testing.fairAI + +
+
+ LinkedIn: {' '} + + @getfair + +
+
+ Website: {' '} + + getfair.ai + +
+
+
+ ); }; @@ -333,7 +397,8 @@ const RequestElement = ({ request }: { request: RequestData }) => {
- {comments.length} {comments.length === 1 ? ' comment' : 'comments'} + {comments.length} {comments.length === 1 ? ' comment' : 'comments'}, 0 + applications
{comments.length === 0 && ( { developer for this request - You are about to suggest yourself as a developer of this request. + You are about to suggest yourself as a developer for this request.
Everything you fill here will be publicly visible in this request!
@@ -432,7 +497,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { } - label='I accept the request as is and want to start right away.' + label='I accept the request as is and am ready to start right away.' />
@@ -583,7 +648,9 @@ const RequestElement = ({ request }: { request: RequestData }) => { > diff --git a/client/src/styles/ui.css b/client/src/styles/ui.css index 262434d0..38f4149b 100644 --- a/client/src/styles/ui.css +++ b/client/src/styles/ui.css @@ -98,12 +98,6 @@ ); } -.underline { - background: linear-gradient(-45deg, #31ab7f50, #422d5c50, #a1205d50, #00000000); - background-size: 600%; - animation: anime 16s linear infinite; -} - h1 { font-size: 24px !important; font-weight: 700 !important; From f427d930d8f6429c90c3cc9d2636b6f3856ede34 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:37:22 +0000 Subject: [PATCH 04/20] feat: more improvements in ux --- client/src/pages/browse-requests.tsx | 124 +++++++++++++++++++++++---- client/src/styles/components.tsx | 7 +- client/src/styles/main.css | 32 +++++++ 3 files changed, 145 insertions(+), 18 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 98e5240b..e1719639 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -55,8 +55,10 @@ import { AddRounded, ChatBubbleRounded, CheckCircleRounded, + CheckRounded, CloseRounded, InfoRounded, + ReplyRounded, StarRounded, } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; @@ -107,6 +109,8 @@ interface Comment { owner: string; timestamp: string; content: string; + showReplyInput?: boolean; + reply?: string; } const CommentElement = ({ comment, request }: { comment: Comment; request: RequestData }) => { @@ -114,22 +118,40 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque window.open(`https://arbiscan.io/address/${comment.owner}`, '_blank'); }, [comment]); + const [changedComment, setChangedComment] = useState(comment); + const [reply, setReply] = useState(''); + + const handleSetShowAddReply = (commentToChange: Comment) => { + if (reply && commentToChange.showReplyInput) { + setReply(''); // clear the reply input + } + + commentToChange.showReplyInput = !commentToChange.showReplyInput; + setChangedComment((prev) => ({ ...prev, reply: reply })); + }; + + const handlePostReplyToComment = (commentToChange: Comment, reply: string) => { + commentToChange.reply = reply; + setChangedComment((prev) => ({ ...prev, reply: reply })); + handleSetShowAddReply(commentToChange); + }; + return ( <>
- +
{' '} {'Commented by '} - {comment.owner.slice(0, 6)}...{comment.owner.slice(-4)} + {changedComment.owner.slice(0, 6)}...{changedComment.owner.slice(-4)} - {` on ${new Date(Number(comment.timestamp) * 1000).toLocaleString()}`} + {` on ${new Date(Number(changedComment.timestamp) * 1000).toLocaleString()}`} - {comment.owner === request.owner && ( + {changedComment.owner === request.owner && (
Request Creator @@ -137,23 +159,57 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque )}
-
{comment.content}
+
{changedComment.content}
+ {!changedComment?.showReplyInput && ( +
+ handleSetShowAddReply(changedComment)} + > + Reply + +
+ )} + + {changedComment?.showReplyInput && ( +
+ handleSetShowAddReply(changedComment)} + > + + + setReply(e.target.value)} + /> + handlePostReplyToComment(changedComment, 'posted reply')} + className='primary plausible-event-name=Request+Reply+Post+Click' + > + Send + +
+ )}
-
+
{'Application / suggestion by '} - {comment.owner.slice(0, 6)}...{comment.owner.slice(-4)} + {changedComment.owner.slice(0, 6)}...{changedComment.owner.slice(-4)} {` on ${new Date(Number(comment.timestamp) * 1000).toLocaleString()}`} - {comment.owner === request.owner && ( + {changedComment.owner === changedComment.owner && (
Application / Suggestion @@ -172,14 +228,6 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque Payment / Deliveries: All at once, right at the start
-
- {' '} - This would be a very difficult project, so I would like to make this suggestion before - accepting it. If we reach an agreement, I can start right away. -
X / Twitter: {' '} @@ -200,6 +248,48 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
+
+ + This would be a very difficult project, so I would like to make this suggestion before + accepting it. If we reach an agreement, I can start right away. +
+ {!changedComment?.showReplyInput && ( +
+ handleSetShowAddReply(changedComment)} + > + Reply + + + + Counter Suggestion + + + Accept Suggestion + +
+ )} + {changedComment?.showReplyInput && ( +
+ handleSetShowAddReply(changedComment)} + > + + + + handleSetShowAddReply(changedComment)} + className='primary plausible-event-name=Request+Reply+Post+Click' + > + Send + +
+ )}
); @@ -578,7 +668,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { (({ theme }) => ({ backgroundColor: 'transparent', border: '2px solid rgb(70,70,70)', - '&:hover': { + '&:hover, &:active, &:focus-within': { backgroundColor: theme.palette.backdropContrast.main, color: '#ffffff', + + img: { + filter: 'invert(1) !important', + transition: '0.2s filter', + }, }, '&:focus': { diff --git a/client/src/styles/main.css b/client/src/styles/main.css index 7a961d30..75f2505f 100644 --- a/client/src/styles/main.css +++ b/client/src/styles/main.css @@ -176,7 +176,39 @@ html { } } +@keyframes fadeInSlideRight { + 0% { + opacity: 0; + transform: translateX(-30px); + } + 100% { + opacity: 1; + transform: translateX(0px); + } +} + +@keyframes fadeInSlideLeft { + 0% { + opacity: 0; + transform: translateX(30px); + } + 100% { + opacity: 1; + transform: translateX(0px); + } +} + .animate-slide-down { opacity: 0; animation: 0.3s fadeInSlideDown ease-out forwards; } + +.animate-slide-right { + opacity: 0; + animation: 0.3s fadeInSlideRight ease-out forwards; +} + +.animate-slide-left { + opacity: 0; + animation: 0.3s fadeInSlideLeft ease-out forwards; +} From a0cd85cb390e86786b95ebfc991c55b951e37aba Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:32:51 +0000 Subject: [PATCH 05/20] feat: finished requests forms --- client/src/pages/browse-requests.tsx | 118 ++++++++++++++-------- client/src/pages/request-solution.tsx | 139 +++++++++++++++++++++----- 2 files changed, 189 insertions(+), 68 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index e1719639..d55abb99 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -119,11 +119,16 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque }, [comment]); const [changedComment, setChangedComment] = useState(comment); + const [replyingTo, setReplyingTo] = useState(''); const [reply, setReply] = useState(''); - const handleSetShowAddReply = (commentToChange: Comment) => { + const handleSetShowAddReply = (commentToChange: Comment, replyToAddress: string) => { if (reply && commentToChange.showReplyInput) { + // reply is open, we are closing it now setReply(''); // clear the reply input + setReplyingTo(''); + } else { + setReplyingTo(replyToAddress); } commentToChange.showReplyInput = !commentToChange.showReplyInput; @@ -133,7 +138,7 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque const handlePostReplyToComment = (commentToChange: Comment, reply: string) => { commentToChange.reply = reply; setChangedComment((prev) => ({ ...prev, reply: reply })); - handleSetShowAddReply(commentToChange); + handleSetShowAddReply(commentToChange, ''); }; return ( @@ -164,7 +169,7 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
handleSetShowAddReply(changedComment)} + onClick={() => handleSetShowAddReply(changedComment, changedComment.owner)} > Reply @@ -172,27 +177,36 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque )} {changedComment?.showReplyInput && ( -
- handleSetShowAddReply(changedComment)} - > - - - setReply(e.target.value)} - /> - handlePostReplyToComment(changedComment, 'posted reply')} - className='primary plausible-event-name=Request+Reply+Post+Click' - > - Send - -
+ <> +
+ Replying to{' '} + + {replyingTo.slice(0, 6)}...{replyingTo.slice(-4)} + {' '} + : +
+
+ handleSetShowAddReply(changedComment, changedComment.owner)} + > + + + setReply(e.target.value)} + /> + handlePostReplyToComment(changedComment, 'posted reply')} + className='primary plausible-event-name=Request+Reply+Post+Click' + > + Send + +
+ )}
@@ -260,35 +274,51 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
handleSetShowAddReply(changedComment)} + onClick={() => handleSetShowAddReply(changedComment, changedComment.owner)} > Reply - - - Counter Suggestion - + + + Deny Suggestion + + Accept Suggestion
)} {changedComment?.showReplyInput && ( -
- handleSetShowAddReply(changedComment)} - > - - - - handleSetShowAddReply(changedComment)} - className='primary plausible-event-name=Request+Reply+Post+Click' - > - Send - -
+ <> +
+ Replying to suggestion from{' '} + + {replyingTo.slice(0, 6)}...{replyingTo.slice(-4)} + {' '} + : +
+
+ handleSetShowAddReply(changedComment, changedComment.owner)} + > + + + setReply(e.target.value)} + /> + handlePostReplyToComment(changedComment, 'posted reply')} + className='primary plausible-event-name=Request+Reply+Post+Click' + > + Send + +
+ )}
diff --git a/client/src/pages/request-solution.tsx b/client/src/pages/request-solution.tsx index 2285b40d..6f9ffbba 100644 --- a/client/src/pages/request-solution.tsx +++ b/client/src/pages/request-solution.tsx @@ -26,7 +26,10 @@ import { Button, Chip, FormControl, + FormControlLabel, MenuItem, + Radio, + RadioGroup, TextField, Typography, } from '@mui/material'; @@ -226,10 +229,16 @@ const RequestSolution = () => { onChange={title.onChange} inputRef={title.ref} onBlur={title.onBlur} - fullWidth variant='outlined' + fullWidth placeholder='Write a title or subject...' - sx={{ backgroundColor: 'white', overflow: 'hidden', borderRadius: '8px' }} + sx={{ + backgroundColor: 'white', + overflow: 'hidden', + borderRadius: '8px', + margin: '0px 16px', + maxWidth: '850px', + }} />
@@ -237,10 +246,11 @@ const RequestSolution = () => { 2. Provide a detailed description - Explain what you need, the context, the problem and what are your initial suggestions or - requisites. You should provide as much information as possible so that whoever reads - this request can get a straightfoward idea of how we can build a custom solution for - you. Remember that everything you write here will be public and visible to everyone. + Explain your project, what you need, the context, the problem and what are your initial + suggestions or pre-requisites. You should provide as much information as needed so that + whoever reads this request can get a straightfoward idea of how we can build a custom + solution for you. Remember that everything you write here will be public and visible to + everyone. { onBlur={description.onBlur} placeholder='Explain your problem and ideas...' multiline - minRows={7} fullWidth + minRows={7} variant='outlined' - sx={{ backgroundColor: 'white', overflow: 'hidden', borderRadius: '8px' }} + sx={{ + backgroundColor: 'white', + overflow: 'hidden', + borderRadius: '8px', + margin: '0px 16px', + maxWidth: '850px', + }} />
@@ -260,12 +276,12 @@ const RequestSolution = () => { 3. Add keywords (categories) that best suit your request - Add at least two or three keywords that best fit your request. It will make it easier - for developers to understand what your need and find your request faster. Some + Add at least two or three keywords that best fit your request/project. It will make it + easier for developers to understand what your need and find your request faster. Some developers like to focus on certain categories. Add up to 6 keywords. -
+
{keywords.map((keyword) => ( { ))}
-
+
{
- 5. What is your initial budget for this request? + 5. Do you have or need the data and/or database for this project? + + + This will clarify if the developers can pick and use a database right up or if they need + to develop one from scratch for your project. This can and should impact the budget and + time/date target. + +
+ + + } + label='Yes, I have all the required data or database and we can use it right away.' + /> + } + label='No, but I will create one myself and then provide it to the developers.' + /> + } + label='No, but we can find the required data or database on the web.' + /> + } + label='No, I need the developers to create the necessary database from scratch.' + /> + + +
+
+ +
+ + 6. Do you have or need an app or website to integrate with this project? + + + If your project needs it, tell us if you already have an app or website that you want + this project to be integrated into, or if developers need to create one for you. This + can and should impact the budget and time/date target. + +
+ + + } + label='Yes, I already have an app or website that I can integrate with this project.' + /> + } + label='No, I need the developers to create and end-to-end solution for this project.' + /> + } + label="No, this project doesn't need any app or website." + /> + + +
+
+ +
+ + 7. What is your expected budget for this project? - Providing an initially planned budget for your request will convince more developers to - try and accept your idea, as they see this budget as their reward for successfuly - completing your request. You can always talk and debate this budget with the developers. + Providing an expected budget for your project will attract more developers, as they see + this budget as their reward for successfuly completing your project. You can always talk + and debate this budget with the developers. -
+
{
- 6. What will be your prefered payment plan to the developers? + 8. What will be your prefered payment plan to the developers? Paying a defined portion of your total budget for each successful partial feature delivery by the developers will make it feel safer for both sides to invest in your - request. You get deliveries in a timely manner, and the developers, their payment + project. You get deliveries in a timely manner, and the developers, their payment portions. -
+
Daily deliveries and payments @@ -340,15 +431,15 @@ const RequestSolution = () => {
- 7. What is your initial date target for this request? + 9. What is your initial date target for this project? - Tell us when you would most likely want the final portion of this request to be + Tell us when you would most likely want the final portion of this project to be delivered. Take in consideration the effort needed to achieve your needs for this - request. This is only a theoretical target, and can always be further discussed with the + project. This is only a theoretical target, and can always be further discussed with the developers. -
+
From 315fd7c59765418dde033b09552e91ab3cdcaf58 Mon Sep 17 00:00:00 2001 From: l-silvestre <43357028+l-silvestre@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:26:21 +0000 Subject: [PATCH 06/20] feat: add extra fields to submitted transaction --- client/src/pages/request-solution.tsx | 171 ++++++++++++++++---------- 1 file changed, 105 insertions(+), 66 deletions(-) diff --git a/client/src/pages/request-solution.tsx b/client/src/pages/request-solution.tsx index 6f9ffbba..e3a58aae 100644 --- a/client/src/pages/request-solution.tsx +++ b/client/src/pages/request-solution.tsx @@ -25,6 +25,7 @@ import { Box, Button, Chip, + FilterOptionsState, FormControl, FormControlLabel, MenuItem, @@ -32,15 +33,28 @@ import { RadioGroup, TextField, Typography, + createFilterOptions } from '@mui/material'; import { useSnackbar } from 'notistack'; -import { useCallback, useState } from 'react'; +import { SyntheticEvent, useCallback, useState } from 'react'; import { UseFormSetValue, useController, useForm, useWatch } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; import { NumericFormat } from 'react-number-format'; -import { DateField, LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import dayjs, { Dayjs } from 'dayjs'; + +interface IRequestSolution { + title: string; + description: string; + keywords: string[]; + needsDb: string; + needsApp: string; + budget: number; + paymentPlan: string; + targetUnixTimestamp: number; +}; const defaultKeywordsList = [ 'AI', @@ -71,9 +85,10 @@ const defaultKeywordsList = [ 'Technology', 'Hardware Control', 'Database Management', - 'Other (specify)', ]; +const filter = createFilterOptions(); + const Keyword = ({ currentKeyword, keywords, @@ -81,11 +96,7 @@ const Keyword = ({ }: { currentKeyword: string; keywords: string[]; - setValue: UseFormSetValue<{ - title: string; - description: string; - keywords: string[]; - }>; + setValue: UseFormSetValue; }) => { const onDelete = useCallback(() => { setValue( @@ -101,26 +112,29 @@ const Keyword = ({ variant='filled' color='primary' className='font-bold' + sx={{ margin:'0px 4px 4px 4px'}} /> ); }; const RequestSolution = () => { const [requestSuccessful, setRequestSuccessfull] = useState(false); - const [newKeyword, setNewKeyword] = useState(''); + const [dateTarge, setDateTarget] = useState(null); + const [autocompleteInputValue, setAutomcompleteInputValue] = useState(''); const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const handleBack = useCallback(() => navigate(-1), [navigate]); - const { control, formState, setValue, handleSubmit } = useForm<{ - title: string; - description: string; - keywords: string[]; - }>({ + const { control, formState, setValue, handleSubmit } = useForm({ defaultValues: { title: '', description: '', - keywords: ['asd'], + keywords: [], + needsDb: '', + needsApp: '', + budget: 0, + paymentPlan: '', + targetUnixTimestamp: 0, }, }); const { field: title } = useController({ control, name: 'title', rules: { required: true } }); @@ -129,15 +143,30 @@ const RequestSolution = () => { name: 'description', rules: { required: true }, }); + const { field: needsDb } = useController({ control, name: 'needsDb', rules: { required: true } }); + const { field: needsApp } = useController({ control, name: 'needsApp', rules: { required: true } }); + const { field: budget } = useController({ control, name: 'budget', rules: { required: true } }); + const { field: paymentPlan } = useController({ + control, + name: 'paymentPlan', + rules: { required: true }, + }); const keywords = useWatch({ control, name: 'keywords' }); - const handleClick = async (data: { title: string; description: string; keywords: string[] }) => { + const handleClick = async (data: IRequestSolution) => { try { const tags = [ { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, { name: TAG_NAMES.operationName, value: 'Request-Solution' }, + { name: 'Request-Title', value: data.title }, + { name: 'Request-Description', value: data.description }, + { name: 'needsDb', value: data.needsDb }, + { name: 'needsApp', value: data.needsApp }, + { name: 'budget', value: data.budget.toString() }, + { name: 'paymentPlan', value: data.paymentPlan }, + { name: 'targetUnixTimestamp', value: data.targetUnixTimestamp.toString() }, { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, ]; @@ -156,24 +185,48 @@ const RequestSolution = () => { } }; - const keyDownHandler = useCallback( - (event: React.KeyboardEvent) => { - if (event.code === 'Enter') { - event.preventDefault(); - setValue('keywords', [...keywords, newKeyword]); - setNewKeyword(''); + const handleNewKeywordAutoCompleteChanged = useCallback( + (event: SyntheticEvent, newValue: string | null) => { + if (newValue && newValue.includes('Add')) { + setValue('keywords', [...keywords, newValue.split('Add')[1].trim()]); + setAutomcompleteInputValue(''); + } else if (newValue) { + setValue('keywords', [...keywords, newValue]); + setAutomcompleteInputValue(''); } - }, - [keywords, newKeyword, setValue, setNewKeyword], + }, + [keywords, setValue], ); - const handleNewKeywordChanged = useCallback( - (event: React.ChangeEvent) => { - setNewKeyword(event.target.value); + const handleAutocompleteInputChanged = useCallback( + (event: SyntheticEvent, newValue: string | null) => { + if (newValue && newValue.includes('Add')) { + setAutomcompleteInputValue(''); + } else { + setAutomcompleteInputValue(newValue || ''); + } + }, [setAutomcompleteInputValue]); + + const handleTargetDataChange = useCallback( + (newValue: Dayjs | null) => { + if (newValue) { + setValue('targetUnixTimestamp', newValue.unix()); + setDateTarget(newValue); + } }, - [setNewKeyword], + [setValue, setDateTarget], ); + const autocompleteFilterOptions = useCallback((options: string[], state: FilterOptionsState) => { + const filtered = filter(options, state); + + if (state.inputValue !== '' && !keywords.includes(state.inputValue)) { + filtered.push(`Add "${state.inputValue}"`); + } + + return filtered.filter((option) => !keywords.includes(option)); + }, [ filter, keywords ]); + if (requestSuccessful) { return ( { developers like to focus on certain categories. Add up to 6 keywords. -
+ {keywords.map((keyword) => ( { setValue={setValue} /> ))} -
+
} /> - - Add
@@ -323,8 +373,9 @@ const RequestSolution = () => {
{
{ thousandSeparator prefix='US$ ' placeholder='Type a value (US$)' + value={budget.value} + onChange={budget.onChange} >
@@ -417,13 +471,13 @@ const RequestSolution = () => {
- - Daily deliveries and payments - Weekly deliveries and payments - Monthly deliveries and payments - Yearly deliveries and payments - All at once, right at the start - All at once, when project ends + + Daily deliveries and payments + Weekly deliveries and payments + Monthly deliveries and payments + Yearly deliveries and payments + All at once, right at the start + All at once, when project ends
@@ -441,25 +495,10 @@ const RequestSolution = () => {
- - + +
-
or
-
- - - Day(s) - Week(s) - Month(s) - Year(s) - -
From 32abb9f47e5984f8043666e58682379b8f0f9e25 Mon Sep 17 00:00:00 2001 From: l-silvestre <43357028+l-silvestre@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:42:44 +0000 Subject: [PATCH 07/20] fix: issue decrypting throwaway private key --- client/src/context/throwaway.tsx | 7 ++++++- client/src/pages/request-solution.tsx | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/context/throwaway.tsx b/client/src/context/throwaway.tsx index d5b5af10..ed488a8b 100644 --- a/client/src/context/throwaway.tsx +++ b/client/src/context/throwaway.tsx @@ -198,7 +198,12 @@ export const ThrowawayProvider = ({ children }: { children: ReactNode }) => { `https://arweave.net/${throwawayData.data.transactions.edges[0].node.id}`, ); const encData = await result.text(); - const decData = await decrypt(encData as `0x${string}`); + const buf = Buffer.from( + encData, + 'utf8' + ); + const encryptedValue = '0x' + buf.toString('hex'); + const decData = await decrypt(encryptedValue as `0x${string}`); setPrivateKey(decData); await setIrys(decData as `0x${string}`); setThrowawayProvider(decData as `0x${string}`); diff --git a/client/src/pages/request-solution.tsx b/client/src/pages/request-solution.tsx index e3a58aae..25cbca3a 100644 --- a/client/src/pages/request-solution.tsx +++ b/client/src/pages/request-solution.tsx @@ -186,7 +186,7 @@ const RequestSolution = () => { }; const handleNewKeywordAutoCompleteChanged = useCallback( - (event: SyntheticEvent, newValue: string | null) => { + (_: SyntheticEvent, newValue: string | null) => { if (newValue && newValue.includes('Add')) { setValue('keywords', [...keywords, newValue.split('Add')[1].trim()]); setAutomcompleteInputValue(''); @@ -199,7 +199,7 @@ const RequestSolution = () => { ); const handleAutocompleteInputChanged = useCallback( - (event: SyntheticEvent, newValue: string | null) => { + (_: SyntheticEvent, newValue: string | null) => { if (newValue && newValue.includes('Add')) { setAutomcompleteInputValue(''); } else { From 9ebc5e3ff5924af75447a03dda6a076db032d1ad Mon Sep 17 00:00:00 2001 From: l-silvestre <43357028+l-silvestre@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:59:42 +0000 Subject: [PATCH 08/20] fix: scroll bouncig on create request --- client/src/components/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/layout.tsx b/client/src/components/layout.tsx index 3f079897..0e987c22 100644 --- a/client/src/components/layout.tsx +++ b/client/src/components/layout.tsx @@ -63,7 +63,7 @@ export default function Layout({ children }: { children: ReactElement }) { disableGutters sx={{ width: '100%', - height: !userScrolledDown && !isSmallScreen ? `calc(100% - ${headerHeight})` : '100%', + height: `calc(100% - ${headerHeight})`, top: userScrolledDown && isSmallScreen ? 0 : headerHeight, position: 'fixed', transition: 'all 0.2s', From a612c685a8e192a53f2e464a5d08f24fd26990ca Mon Sep 17 00:00:00 2001 From: l-silvestre <43357028+l-silvestre@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:33:50 +0100 Subject: [PATCH 09/20] feat: advances in showing new request solutions --- client/src/components/make-request-banner.tsx | 55 +++++++ client/src/hooks/useModels.tsx | 136 ------------------ client/src/interfaces/common.ts | 11 ++ client/src/pages/browse-requests.tsx | 75 ++-------- client/src/pages/home.tsx | 51 +------ client/src/pages/request-solution.tsx | 20 +-- 6 files changed, 88 insertions(+), 260 deletions(-) create mode 100644 client/src/components/make-request-banner.tsx delete mode 100644 client/src/hooks/useModels.tsx diff --git a/client/src/components/make-request-banner.tsx b/client/src/components/make-request-banner.tsx new file mode 100644 index 00000000..b4b8b37f --- /dev/null +++ b/client/src/components/make-request-banner.tsx @@ -0,0 +1,55 @@ +import { StyledMuiButton } from '@/styles/components'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; +import InfoRounded from '@mui/icons-material/InfoRounded'; +import LibraryAddRoundedIcon from '@mui/icons-material/LibraryAddRounded'; + +const MakeRequestBanner = ({ smallScreen }: { smallScreen: boolean }) => { + const navigate = useNavigate(); + const openRequestsRoute = useCallback(() => navigate('/request'), [navigate]); + + return ( +
+
+ + + Are you looking for custom made, tailored solutions for your own projects? +
+ Create your request listing, define your budget and quickly get amazing solutions tailored + for you by the trusted FairAI community members. +
+ + + + Create a request + +
+
+ ); +}; + +export default MakeRequestBanner; \ No newline at end of file diff --git a/client/src/hooks/useModels.tsx b/client/src/hooks/useModels.tsx deleted file mode 100644 index 060bf8c1..00000000 --- a/client/src/hooks/useModels.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Fair Protocol, open source decentralised inference marketplace for artificial intelligence. - * Copyright (C) 2023 Fair Protocol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -import { TAG_NAMES } from '@/constants'; -import FilterContext from '@/context/filter'; -import { IContractEdge } from '@/interfaces/arweave'; -import { commonUpdateQuery, findTagsWithKeyword, findTag } from '@/utils/common'; -import { useQuery, NetworkStatus } from '@apollo/client'; -import { RefObject, useState, useContext, useEffect, useMemo } from 'react'; -import useOnScreen from './useOnScreen'; -import FairSDKWeb from '@fair-protocol/sdk/web'; -import _ from 'lodash'; -import Stamps, { CountResult } from '@permaweb/stampjs'; -import { WarpFactory } from 'warp-contracts'; -import Arweave from 'arweave'; - -const useModels = (target?: RefObject, featuredElements?: number) => { - const [hasNextPage, setHasNextPage] = useState(false); - const [txs, setTxs] = useState([]); - const [featuredTxs, setFeaturedTxs] = useState([]); - const [validTxs, setValidTxs] = useState([]); - const [filtering, setFiltering] = useState(false); - const [txsCountsMap, setTxsCountsMap] = useState>(new Map()); - - const filterValue = useContext(FilterContext); - const isOnScreen = useOnScreen(target); - - const elementsPerPage = 5; - const defaultFeaturedElements = 3; - const queryObject = FairSDKWeb.utils.getModelsQuery(elementsPerPage); - const { data, loading, error, fetchMore, networkStatus } = useQuery(queryObject.query, { - variables: queryObject.variables, - }); - - const loadingOrFiltering = useMemo(() => filtering || loading, [filtering, loading]); - - const transformCountsToObjectMap = (counts: CountResult[]): Map => - new Map(Object.entries(counts)); - - const totalStamps = async (targetTxs: (string | undefined)[]) => { - try { - const filteredTxsIds = targetTxs.filter((txId) => txId !== undefined) as string[]; - const stampsInstance = Stamps.init({ - warp: WarpFactory.forMainnet(), - arweave: Arweave.init({}), - wallet: window.arweaveWallet, - dre: 'https://dre-u.warp.cc/contract', - graphql: 'https://arweave.net/graphql', - }); - const counts = await stampsInstance.counts(filteredTxsIds); - - return transformCountsToObjectMap(counts); - } catch (errorObj) { - return new Map(); - } - }; - - useEffect(() => { - if (data && networkStatus === NetworkStatus.ready) { - (async () => { - setFiltering(true); - const filtered = await FairSDKWeb.utils.modelsFilter(data.transactions.edges); - const targetTxs = filtered.map((el) => findTag(el, 'modelTransaction')); - const mapTxsCountStamps = await totalStamps(targetTxs); - setTxsCountsMap(mapTxsCountStamps); - setHasNextPage(data.transactions.pageInfo.hasNextPage); - setTxs(filtered); - setValidTxs(filtered); - const newFilteredTxs = filtered.slice(0, featuredElements ?? defaultFeaturedElements); - if (!_.isEqual(newFilteredTxs, featuredTxs)) { - setFeaturedTxs(newFilteredTxs); - } - setFiltering(false); - })(); - } - }, [data]); - - useEffect(() => { - if (data) { - setFiltering(true); - const filtered: IContractEdge[] = validTxs.filter( - (el: IContractEdge) => - filterValue.trim() === '' || - findTagsWithKeyword( - el, - [TAG_NAMES.modelName, TAG_NAMES.description, TAG_NAMES.modelCategory], - filterValue, - ), - ); - setTxs(filtered); - setFiltering(false); - } - }, [filterValue]); - - useEffect(() => { - if (isOnScreen && hasNextPage) { - (async () => { - await fetchMore({ - variables: { - after: - data.transactions.edges.length > 0 - ? data.transactions.edges[data.transactions.edges.length - 1].cursor - : undefined, - }, - updateQuery: commonUpdateQuery, - }); - })(); - } - }, [isOnScreen, data, hasNextPage]); - - return { - loading: loadingOrFiltering, - txs, - txsCountsMap, - isOnScreen, - featuredTxs, - error, - }; -}; - -export default useModels; diff --git a/client/src/interfaces/common.ts b/client/src/interfaces/common.ts index 2752a6d2..575937b9 100644 --- a/client/src/interfaces/common.ts +++ b/client/src/interfaces/common.ts @@ -98,3 +98,14 @@ export interface OperatorData { operatorFee: number; solutionId: string; } + +export interface IRequestSolution { + title: string; + description: string; + keywords: string[]; + needsDb: string; + needsApp: string; + budget: number; + paymentPlan: string; + targetUnixTimestamp: number; +}; \ No newline at end of file diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index d55abb99..9fa91232 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -50,7 +50,6 @@ import { StyledMuiButton } from '@/styles/components'; import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded'; import useScroll from '@/hooks/useScroll'; import useWindowDimensions from '@/hooks/useWindowDimensions'; -import LibraryAddRoundedIcon from '@mui/icons-material/LibraryAddRounded'; import { AddRounded, ChatBubbleRounded, @@ -61,10 +60,12 @@ import { ReplyRounded, StarRounded, } from '@mui/icons-material'; -import { useNavigate } from 'react-router-dom'; import { NumericFormat } from 'react-number-format'; import { DateField, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { IRequestSolution } from '@/interfaces/common'; +import dayjs from 'dayjs'; +import MakeRequestBanner from '@/components/make-request-banner'; interface IrysTx { id: string; @@ -75,10 +76,7 @@ interface IrysTx { address: string; } -interface RequestData { - title: string; - description: string; - keywords: string[]; +interface RequestData extends IRequestSolution { id: string; owner: string; timestamp: string; @@ -502,16 +500,19 @@ const RequestElement = ({ request }: { request: RequestData }) => {
- Planned budget: US$ 5,000.00 + Planned budget:{request.budget}
- Payment / Deliveries: Monthly + Payment / Deliveries:{request.paymentPlan}
- Date target: 02/28/2025 + Date target:{dayjs(request.targetUnixTimestamp * 1000).format('MM/YYYY')}
- Time budget: 4 months + Application Development:{request.needsApp} +
+
+ Database Configuration:{request.needsDb}
@@ -793,54 +794,6 @@ const RequestElement = ({ request }: { request: RequestData }) => { ); }; -const MakeRequestMessage = ({ smallScreen }: { smallScreen: boolean }) => { - const navigate = useNavigate(); - const openRequestsRoute = useCallback(() => navigate('/request'), [navigate]); - - return ( -
-
- - - Are you looking for custom made, tailored solutions for your own projects? -
- Create your request listing, define your budget and quickly get amazing solutions tailored - for you by the trusted FairAI community members. -
- - - - Create a request - -
-
- ); -}; - const BrowseRequests = () => { const [requests, setRequests] = useState([]); const [filtering, setFiltering] = useState(false); @@ -859,8 +812,8 @@ const BrowseRequests = () => { const { data, loading } = useQuery(irysQuery, { variables: { tags: [ - { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME] }, - { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION] }, + { name: TAG_NAMES.protocolName, values: ['FairAI-test'] }, + { name: TAG_NAMES.protocolVersion, values: ['test'] }, { name: TAG_NAMES.operationName, values: ['Request-Solution'] }, ], first: 10, @@ -948,7 +901,7 @@ const BrowseRequests = () => { )} - {!isLoading && } + {!isLoading && } {requests.length === 0 && !isLoading && ( diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index c82c038a..fb910a71 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -20,6 +20,7 @@ import { Box, Container, Grid, InputBase, Typography, useTheme } from '@mui/mate import '@/styles/ui.css'; import useSolutions from '@/hooks/useSolutions'; import LoadingCard from '@/components/loading-card'; +import MakeRequestBanner from '@/components/make-request-banner'; import { ChangeEvent, RefObject, @@ -46,7 +47,6 @@ import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded'; import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; import useWindowDimensions from '@/hooks/useWindowDimensions'; import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded'; -import { InfoRounded } from '@mui/icons-material'; const WarningMessage = ({ smallScreen, @@ -184,53 +184,6 @@ const WarningMessage = ({ } }; -const B2bMessage = ({ smallScreen }: { smallScreen: boolean }) => { - const navigate = useNavigate(); - const openRequestsRoute = useCallback(() => navigate('/browse'), [navigate]); - - return ( -
- - - Are you a business or company looking for custom made, tailored solutions for your own - projects? -
- Make a request, define your budget and quickly get amazing solutions tailored for you by the - trusted FairAI community members. -
- - - - Go to requests - -
- ); -}; - export default function Home() { const target = useRef(null); const { loading, txs, error, refetch } = useSolutions(target); @@ -393,7 +346,7 @@ export default function Home() { {!error && !loading && (
- +
)} diff --git a/client/src/pages/request-solution.tsx b/client/src/pages/request-solution.tsx index 25cbca3a..aff93010 100644 --- a/client/src/pages/request-solution.tsx +++ b/client/src/pages/request-solution.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; +import { /* PROTOCOL_NAME, PROTOCOL_VERSION, */ TAG_NAMES } from '@/constants'; import { StyledMuiButton } from '@/styles/components'; import { postOnArweave } from '@fairai/evm-sdk'; import Close from '@mui/icons-material/Close'; @@ -44,17 +44,7 @@ import { NumericFormat } from 'react-number-format'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs, { Dayjs } from 'dayjs'; - -interface IRequestSolution { - title: string; - description: string; - keywords: string[]; - needsDb: string; - needsApp: string; - budget: number; - paymentPlan: string; - targetUnixTimestamp: number; -}; +import { IRequestSolution } from '@/interfaces/common'; const defaultKeywordsList = [ 'AI', @@ -157,8 +147,10 @@ const RequestSolution = () => { const handleClick = async (data: IRequestSolution) => { try { const tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, + /* { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, */ + { name: TAG_NAMES.protocolName, values: ['FairAI-test'] }, + { name: TAG_NAMES.protocolVersion, values: ['test'] }, { name: TAG_NAMES.operationName, value: 'Request-Solution' }, { name: 'Request-Title', value: data.title }, { name: 'Request-Description', value: data.description }, From 641e98b252d8a12c0760c0884dde809ea4946b0e Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:29:10 +0100 Subject: [PATCH 10/20] feat: dev of requests data --- client/src/interfaces/common.ts | 4 +- client/src/pages/browse-requests.tsx | 31 +++++++--- client/src/pages/request-solution.tsx | 70 ++++++++++++++++------- client/src/utils/requestsPipeFunctions.ts | 50 ++++++++++++++++ 4 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 client/src/utils/requestsPipeFunctions.ts diff --git a/client/src/interfaces/common.ts b/client/src/interfaces/common.ts index 575937b9..03e152cf 100644 --- a/client/src/interfaces/common.ts +++ b/client/src/interfaces/common.ts @@ -105,7 +105,7 @@ export interface IRequestSolution { keywords: string[]; needsDb: string; needsApp: string; - budget: number; + budget: string; paymentPlan: string; targetUnixTimestamp: number; -}; \ No newline at end of file +} diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 9fa91232..0010901b 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -16,6 +16,12 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ +import { + databaseConfigType, + needsAppConfig, + paymentPlanBrowseTag, + paymentPlanType, +} from '@/utils/requestsPipeFunctions'; import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; import { gql, useQuery } from '@apollo/client'; import Close from '@mui/icons-material/Close'; @@ -438,7 +444,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { animate={{ opacity: 1, y: 0 }} className='w-full' > - + @@ -500,19 +506,24 @@ const RequestElement = ({ request }: { request: RequestData }) => {
- Planned budget:{request.budget} + Planned budget: + {request.budget}
- Payment / Deliveries:{request.paymentPlan} + Payment / Deliveries: + {paymentPlanType(request.paymentPlan)}
- Date target:{dayjs(request.targetUnixTimestamp * 1000).format('MM/YYYY')} + Date target: + {dayjs(request.targetUnixTimestamp * 1000).format('MM/YYYY')}
- Application Development:{request.needsApp} + Application Development: + {needsAppConfig(request.needsApp)}
- Database Configuration:{request.needsDb} + Database: + {databaseConfigType(request.needsDb)}
@@ -527,7 +538,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { display={'flex'} justifyContent={'center'} fontWeight={600} - className='bg-white rounded-lg py-3' + className='bg-neutral-100 rounded-lg py-3' > {'No comments yet.'} @@ -774,7 +785,11 @@ const RequestElement = ({ request }: { request: RequestData }) => { }, 0 applications`} color='secondary' /> - +
{request.keywords.map((keyword) => ( ); }; @@ -122,7 +122,7 @@ const RequestSolution = () => { keywords: [], needsDb: '', needsApp: '', - budget: 0, + budget: '', paymentPlan: '', targetUnixTimestamp: 0, }, @@ -134,7 +134,11 @@ const RequestSolution = () => { rules: { required: true }, }); const { field: needsDb } = useController({ control, name: 'needsDb', rules: { required: true } }); - const { field: needsApp } = useController({ control, name: 'needsApp', rules: { required: true } }); + const { field: needsApp } = useController({ + control, + name: 'needsApp', + rules: { required: true }, + }); const { field: budget } = useController({ control, name: 'budget', rules: { required: true } }); const { field: paymentPlan } = useController({ control, @@ -186,7 +190,7 @@ const RequestSolution = () => { setValue('keywords', [...keywords, newValue]); setAutomcompleteInputValue(''); } - }, + }, [keywords, setValue], ); @@ -197,7 +201,9 @@ const RequestSolution = () => { } else { setAutomcompleteInputValue(newValue || ''); } - }, [setAutomcompleteInputValue]); + }, + [setAutomcompleteInputValue], + ); const handleTargetDataChange = useCallback( (newValue: Dayjs | null) => { @@ -209,15 +215,18 @@ const RequestSolution = () => { [setValue, setDateTarget], ); - const autocompleteFilterOptions = useCallback((options: string[], state: FilterOptionsState) => { - const filtered = filter(options, state); + const autocompleteFilterOptions = useCallback( + (options: string[], state: FilterOptionsState) => { + const filtered = filter(options, state); - if (state.inputValue !== '' && !keywords.includes(state.inputValue)) { - filtered.push(`Add "${state.inputValue}"`); - } + if (state.inputValue !== '' && !keywords.includes(state.inputValue)) { + filtered.push(`Add "${state.inputValue}"`); + } - return filtered.filter((option) => !keywords.includes(option)); - }, [ filter, keywords ]); + return filtered.filter((option) => !keywords.includes(option)); + }, + [filter, keywords], + ); if (requestSuccessful) { return ( @@ -230,7 +239,7 @@ const RequestSolution = () => { alignItems={'center'} overflow={'auto'} > - Your Request Has been registered. + Your Request Has Been Registered. Thank you for your collaboration.
@@ -389,6 +396,11 @@ const RequestSolution = () => { control={} label='No, I need the developers to create the necessary database from scratch.' /> + } + label='This project does not need a database or specific data.' + />
@@ -423,7 +435,12 @@ const RequestSolution = () => { } - label="No, this project doesn't need any app or website." + label='No, but we can find one on the web that we can use for this project.' + /> + } + label='This project does not need any app or website.' /> @@ -463,7 +480,14 @@ const RequestSolution = () => {
- + Daily deliveries and payments Weekly deliveries and payments Monthly deliveries and payments @@ -488,7 +512,13 @@ const RequestSolution = () => {
- +
diff --git a/client/src/utils/requestsPipeFunctions.ts b/client/src/utils/requestsPipeFunctions.ts new file mode 100644 index 00000000..0cc086ac --- /dev/null +++ b/client/src/utils/requestsPipeFunctions.ts @@ -0,0 +1,50 @@ +// full payment info to show when reading a single request info +export function paymentPlanType(paymentPlanTag: string) { + const paymentPlans = { + daily: 'Daily deliveries and payments', + weekly: 'Weekly deliveries and payments', + monthly: 'Monthly deliveries and payments', + yearly: 'Yearly deliveries and payments', + 'full-at-start': 'All at once, right at the start', + 'full-at-end': 'All at once, when project ends', + }; + + return paymentPlans[paymentPlanTag as keyof typeof paymentPlans] ?? '(Not Provided)'; +} + +// compact info to show on the pills in browse requests +export function paymentPlanBrowseTag(paymentPlanTag: string) { + const paymentPlans = { + daily: 'Daily Payments', + weekly: 'Weekly Payments', + monthly: 'Monthly Payments', + yearly: 'Yearly Payments', + 'full-at-start': 'Payment in Full', + 'full-at-end': 'Payment in Full', + }; + + return paymentPlans[paymentPlanTag as keyof typeof paymentPlans] ?? '(Not Provided)'; +} + +export function databaseConfigType(databaseTag: string) { + const databaseOptions = { + yes: 'Creator has the required data or database and we can use it.', + 'no-will-create-myself': 'None yet, creator will create on and then provide it.', + 'no-but-find-on-web': 'None yet, but we can find the data or database on the web.', + 'no-devs-create-from-scratch': 'None yet, need developers to create one from scratch.', + 'not-needed': 'Not needed for this project.', + }; + + return databaseOptions[databaseTag as keyof typeof databaseOptions] ?? '(Not Provided)'; +} + +export function needsAppConfig(needsAppTag: string) { + const needsAppOptions = { + yes: 'Creator already has an app/website to integrate with this request.', + 'no-need-create-e2e': 'None yet, we need to create a new E2E solution.', + 'no-but-find-on-web': 'None yet, but we can find one on the web.', + 'not-needed': 'Not needed for this project.', + }; + + return needsAppOptions[needsAppTag as keyof typeof needsAppOptions] ?? '(Not Provided)'; +} From 73529947c526fcc9747141075c3f0815c9e7b739 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:22:34 +0100 Subject: [PATCH 11/20] feat: more dev into requests --- client/src/pages/browse-requests.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 0010901b..d1eeab3c 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -529,8 +529,7 @@ const RequestElement = ({ request }: { request: RequestData }) => {
- {comments.length} {comments.length === 1 ? ' comment' : 'comments'}, 0 - applications + {comments.length} {comments.length === 1 ? ' comment' : 'comments'}
{comments.length === 0 && ( { > Date: Wed, 9 Apr 2025 12:00:31 +0100 Subject: [PATCH 12/20] feat: added logo image file --- client/public/fairai_logo_whitebg.png | Bin 0 -> 37727 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/public/fairai_logo_whitebg.png diff --git a/client/public/fairai_logo_whitebg.png b/client/public/fairai_logo_whitebg.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5e38551ba53001fec0be8e842d3db161b6011d GIT binary patch literal 37727 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Ҏ@IXR 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 literal 0 HcmV?d00001 From fa331d214af814a6b43b7f94d4eb81c0e147590b Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:13:37 +0100 Subject: [PATCH 13/20] feat: improve requests comments --- .husky/commit-msg | 1 + .husky/pre-commit | 1 + client/src/pages/browse-requests.tsx | 176 ++++++++++++++------------- 3 files changed, 96 insertions(+), 82 deletions(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index c160a771..a5a6ecaa 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npx --no -- commitlint --edit ${1} +npx --no -- commitlint --edit ${1} diff --git a/.husky/pre-commit b/.husky/pre-commit index 1c3f56b4..87689ec6 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,3 +3,4 @@ echo Checking lint... npx --no -- lint-staged +npx --no -- lint-staged diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index d1eeab3c..f6cb66a7 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -45,6 +45,7 @@ import { Checkbox, FormControl, MenuItem, + CircularProgress, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SendIcon from '@mui/icons-material/Send'; @@ -60,7 +61,6 @@ import { AddRounded, ChatBubbleRounded, CheckCircleRounded, - CheckRounded, CloseRounded, InfoRounded, ReplyRounded, @@ -110,6 +110,7 @@ const irysQuery = gql` `; interface Comment { + id: string; owner: string; timestamp: string; content: string; @@ -126,22 +127,39 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque const [replyingTo, setReplyingTo] = useState(''); const [reply, setReply] = useState(''); - const handleSetShowAddReply = (commentToChange: Comment, replyToAddress: string) => { + const handleSetShowAddReply = (commentToChange: Comment, replyToCommentId: string) => { if (reply && commentToChange.showReplyInput) { // reply is open, we are closing it now setReply(''); // clear the reply input setReplyingTo(''); } else { - setReplyingTo(replyToAddress); + setReplyingTo(replyToCommentId); } commentToChange.showReplyInput = !commentToChange.showReplyInput; setChangedComment((prev) => ({ ...prev, reply: reply })); }; - const handlePostReplyToComment = (commentToChange: Comment, reply: string) => { - commentToChange.reply = reply; - setChangedComment((prev) => ({ ...prev, reply: reply })); + const handlePostReplyToComment = async (commentToChange: Comment, reply: string) => { + // const newComment = { + // owner: currentAddress, + // timestamp: (Date.now() / 1000).toString(), + // content: reply, + // }; + + const tags = [ + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, + { name: TAG_NAMES.operationName, value: 'Comment' }, + { name: 'Comment-For', value: request.id }, + { name: 'Comment-Reply-To-Comment', value: replyingTo }, + { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, + ]; + + await postOnArweave(reply, tags); + + // setComments((prev) => [...prev, newComment]); + handleSetShowAddReply(commentToChange, ''); }; @@ -152,7 +170,7 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque +
{changedComment.content}
+ {!changedComment?.showReplyInput && (
handleSetShowAddReply(changedComment, changedComment.owner)} + onClick={() => handleSetShowAddReply(changedComment, changedComment.id)} > Reply @@ -248,20 +268,35 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
- Website: {' '} - + Website: + getfair.ai
@@ -282,48 +317,8 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque > Reply - - - Deny Suggestion - - - - Accept Suggestion -
)} - {changedComment?.showReplyInput && ( - <> -
- Replying to suggestion from{' '} - - {replyingTo.slice(0, 6)}...{replyingTo.slice(-4)} - {' '} - : -
-
- handleSetShowAddReply(changedComment, changedComment.owner)} - > - - - setReply(e.target.value)} - /> - handlePostReplyToComment(changedComment, 'posted reply')} - className='primary plausible-event-name=Request+Reply+Post+Click' - > - Send - -
- - )}
); @@ -332,6 +327,7 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque const RequestElement = ({ request }: { request: RequestData }) => { const [open, setOpen] = useState(false); const [comments, setComments] = useState([]); + const [commentsLoadingAnim, setCommentsLoadingAnim] = useState(false); const [newComment, setNewComment] = useState(''); const [showAddComment, setShowAddComment] = useState(false); const [showAcceptAsDev, setShowAcceptAsDev] = useState(false); @@ -364,23 +360,24 @@ const RequestElement = ({ request }: { request: RequestData }) => { ); const handleNewComment = useCallback(async () => { - const comment = { - owner: currentAddress, - timestamp: (Date.now() / 1000).toString(), - content: newComment, - }; + // const comment = { + // owner: currentAddress, + // timestamp: (Date.now() / 1000).toString(), + // content: newComment, + // }; const tags = [ { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, { name: TAG_NAMES.operationName, value: 'Comment' }, { name: 'Comment-For', value: request.id }, + { name: 'Comment-Reply-To-Comment', value: request.id }, { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, ]; await postOnArweave(newComment, tags); - setComments((prev) => [...prev, comment]); + // setComments((prev) => [...prev, comment]); setNewComment(''); }, [request, newComment, currentAddress, setComments, setNewComment]); @@ -388,11 +385,13 @@ const RequestElement = ({ request }: { request: RequestData }) => { if (commentsData && commentsData.transactions.edges) { (async () => { const allComments = []; + setCommentsLoadingAnim(true); for (const tx of commentsData.transactions.edges) { const res = await fetch(`https://arweave.net/${tx.node.id}`); const data = await res.text(); allComments.push({ + id: tx.node.id, owner: tx.node.address, timestamp: tx.node.tags.find( @@ -403,6 +402,9 @@ const RequestElement = ({ request }: { request: RequestData }) => { } setComments(allComments); + setTimeout(() => { + setCommentsLoadingAnim(false); + }, 5000); })(); } }, [commentsData, setComments]); @@ -569,10 +571,9 @@ const RequestElement = ({ request }: { request: RequestData }) => { - + - - Accept and develop this request + I am interested in developing this request
@@ -597,7 +598,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { onClick={handleNewComment} className='primary plausible-event-name=Request+Comment+Click' > - Add + Submit
)} @@ -605,21 +606,23 @@ const RequestElement = ({ request }: { request: RequestData }) => { {showAcceptAsDev && (
- Assign yourself as a - developer for this request + Request to be a developer + for this request - You are about to suggest yourself as a developer for this request. + You are about to suggest yourself as a developer or one of the + developers for this request.
- Everything you fill here will be publicly visible in this request! + Everything you fill here will be publicly visible in this request + comments!
You can accept everything as is, or suggest a different budget, different target date, time needed or a different payment plan.
- The request creator will be able to counter your offer with a different - one, or accept it. -
You can also leave any additional info or comments in the comment - box, as needed. + The request creator will be able to make a response to your request and + let you know more details. +
You can also leave any additional info or comments in the + "Anything else?" box, as needed.
You suggestion / proposal @@ -633,6 +636,8 @@ const RequestElement = ({ request }: { request: RequestData }) => {
+ Budgets and date targets suggestion +
{ -
- Time budget / date target suggestion - +
{
- Ways to contact you + Ways to contact you
{ className='w-full max-w-[300px]' InputProps={{ startAdornment: <>@ }} /> + @ }} + />
- Anything else? + Anything else? { > } ${ + comments.length + } ${comments.length === 1 ? ' comment' : ' comments'}`} color='secondary' /> Date: Thu, 17 Apr 2025 00:26:25 +0100 Subject: [PATCH 14/20] feat: developments in requests - replies --- client/src/constants.ts | 3 + client/src/pages/browse-requests.tsx | 416 +++++++++++++++++---------- 2 files changed, 270 insertions(+), 149 deletions(-) diff --git a/client/src/constants.ts b/client/src/constants.ts index 7032ff70..a653c473 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -45,6 +45,9 @@ export const OLD_PROTOCOL_VERSION = '1.0'; export const PROTOCOL_NAME = 'FairAI'; export const PROTOCOL_VERSION = '2.0'; +export const PROTOCOL_NAME_TEST = 'FairAI-test'; +export const PROTOCOL_VERSION_TEST = 'test'; + export const MARKETPLACE_FEE = '0.5'; // u export const OPERATOR_REGISTRATION_AR_FEE = '0.05'; // u diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index f6cb66a7..5d417a6e 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -22,7 +22,7 @@ import { paymentPlanBrowseTag, paymentPlanType, } from '@/utils/requestsPipeFunctions'; -import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; +import { PROTOCOL_NAME_TEST, PROTOCOL_VERSION_TEST, TAG_NAMES } from '@/constants'; import { gql, useQuery } from '@apollo/client'; import Close from '@mui/icons-material/Close'; import { @@ -72,6 +72,7 @@ import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { IRequestSolution } from '@/interfaces/common'; import dayjs from 'dayjs'; import MakeRequestBanner from '@/components/make-request-banner'; +import { ITag } from '@/interfaces/arweave'; interface IrysTx { id: string; @@ -115,85 +116,190 @@ interface Comment { timestamp: string; content: string; showReplyInput?: boolean; - reply?: string; + replies?: Comment[]; + tags: ITag[]; } -const CommentElement = ({ comment, request }: { comment: Comment; request: RequestData }) => { +const CommentElement = ({ + comment, + request, + refetchComments, + isReply, + replyToUserAddress, + commentType, + replyChainMainParentId, +}: { + comment: Comment; + request: RequestData; + isReply: boolean; + replyToUserAddress: string; + replyChainMainParentId: string; + commentType: 'text' | 'application'; + refetchComments: () => void; +}) => { const handleAddressClick = useCallback(() => { window.open(`https://arbiscan.io/address/${comment.owner}`, '_blank'); }, [comment]); const [changedComment, setChangedComment] = useState(comment); - const [replyingTo, setReplyingTo] = useState(''); + const [replyingToCommentId, setReplyingToCommentId] = useState(''); + const [replyingToUserAddress, setReplyingToUserAddress] = useState(''); const [reply, setReply] = useState(''); - const handleSetShowAddReply = (commentToChange: Comment, replyToCommentId: string) => { + const handleSetShowAddReply = ( + commentToChange: Comment, + replyToCommentId: string, + replyToCommentOwnerAddress: string, + ) => { if (reply && commentToChange.showReplyInput) { // reply is open, we are closing it now setReply(''); // clear the reply input - setReplyingTo(''); + setReplyingToCommentId(''); + setReplyingToUserAddress(''); } else { - setReplyingTo(replyToCommentId); + setReplyingToCommentId(replyToCommentId); + setReplyingToUserAddress(replyToCommentOwnerAddress); } commentToChange.showReplyInput = !commentToChange.showReplyInput; setChangedComment((prev) => ({ ...prev, reply: reply })); }; - const handlePostReplyToComment = async (commentToChange: Comment, reply: string) => { - // const newComment = { - // owner: currentAddress, - // timestamp: (Date.now() / 1000).toString(), - // content: reply, - // }; - + const handlePostReplyToComment = async ( + commentToChange: Comment, + reply: string, + isSuggestion: boolean = false, + ) => { const tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, { name: TAG_NAMES.operationName, value: 'Comment' }, { name: 'Comment-For', value: request.id }, - { name: 'Comment-Reply-To-Comment', value: replyingTo }, + { name: 'Replying-To-Comment-Id', value: replyingToCommentId }, + { name: 'Replying-To-User-Address', value: replyingToUserAddress }, + { name: 'Reply-Chain-Main-Parent-Id', value: replyChainMainParentId }, + { name: 'Is-Suggestion', value: isSuggestion.toString() }, { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, ]; await postOnArweave(reply, tags); - // setComments((prev) => [...prev, newComment]); + refetchComments(); - handleSetShowAddReply(commentToChange, ''); + handleSetShowAddReply(commentToChange, '', ''); }; return ( - <> -
- +
+ {isReply &&
} + +
+
- {' '} + {commentType === 'text' && ( + + )} + + {commentType === 'application' && } + - {'Comment by '} + {!isReply ? 'Comment by ' : 'Reply by '} {changedComment.owner.slice(0, 6)}...{changedComment.owner.slice(-4)} + {isReply && replyToUserAddress && ( + <> + {' to '} + + + {replyToUserAddress.slice(0, 6)}...{replyToUserAddress.slice(-4)} + + + + )} {` on ${new Date(Number(changedComment.timestamp) * 1000).toLocaleString()}`} {changedComment.owner === request.owner && ( -
+
Request Creator
)} + {commentType === 'application' && ( +
+ Application / Suggestion +
+ )}
-
{changedComment.content}
+ {commentType === 'application' && ( +
+
+
+ Budget suggestion: US$ 6,000.00 +
+
+ Time suggestion: 6 month(s) +
+
+ Payment / Deliveries: All at once, right at the start +
+
+
+
+ X / Twitter: + + @getfairai + +
+
+ LinkedIn: + + @getfairai + +
+
+ Website: + + getfair.ai + +
+
+
+ )} + +
{changedComment.content}
{!changedComment?.showReplyInput && (
handleSetShowAddReply(changedComment, changedComment.id)} + onClick={() => + handleSetShowAddReply(changedComment, changedComment.id, changedComment.owner) + } > Reply @@ -205,14 +311,16 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
Replying to{' '} - {replyingTo.slice(0, 6)}...{replyingTo.slice(-4)} + {replyingToUserAddress.slice(0, 6)}...{replyingToUserAddress.slice(-4)} {' '} :
handleSetShowAddReply(changedComment, changedComment.owner)} + onClick={() => + handleSetShowAddReply(changedComment, changedComment.id, changedComment.owner) + } > @@ -224,7 +332,7 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque onChange={(e) => setReply(e.target.value)} /> handlePostReplyToComment(changedComment, 'posted reply')} + onClick={() => handlePostReplyToComment(changedComment, reply, false)} className='primary plausible-event-name=Request+Reply+Post+Click' > Send @@ -232,95 +340,9 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
)} - -
- -
-
- - - {'Application / suggestion by '} - - - {changedComment.owner.slice(0, 6)}...{changedComment.owner.slice(-4)} - - - {` on ${new Date(Number(comment.timestamp) * 1000).toLocaleString()}`} - - {changedComment.owner === changedComment.owner && ( - -
- Application / Suggestion -
-
- )}
-
-
- Budget suggestion: US$ 6,000.00 -
-
- Time suggestion: 6 month(s) -
-
- Payment / Deliveries: All at once, right at the start -
-
-
-
- X / Twitter: - - @getfairai - -
-
- LinkedIn: - - @getfairai - -
-
- Website: - - getfair.ai - -
-
-
- - This would be a very difficult project, so I would like to make this suggestion before - accepting it. If we reach an agreement, I can start right away. -
- {!changedComment?.showReplyInput && ( -
- handleSetShowAddReply(changedComment, changedComment.owner)} - > - Reply - -
- )}
- +
); }; @@ -328,6 +350,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { const [open, setOpen] = useState(false); const [comments, setComments] = useState([]); const [commentsLoadingAnim, setCommentsLoadingAnim] = useState(false); + const [commentsAmountTotal, setCommentsAmountTotal] = useState(0); const [newComment, setNewComment] = useState(''); const [showAddComment, setShowAddComment] = useState(false); const [showAcceptAsDev, setShowAcceptAsDev] = useState(false); @@ -339,11 +362,15 @@ const RequestElement = ({ request }: { request: RequestData }) => { const handleShowNewComment = () => setShowAddComment(!showAddComment); const handleShowAcceptAsDev = () => setShowAcceptAsDev(!showAcceptAsDev); - const { data: commentsData } = useQuery(irysQuery, { + const { + data: commentsData, + refetch, + loading: loadingQuery, + } = useQuery(irysQuery, { variables: { tags: [ - { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME] }, - { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION] }, + { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME_TEST] }, + { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION_TEST] }, { name: TAG_NAMES.operationName, values: ['Comment'] }, { name: 'Comment-For', values: [request.id] }, ], @@ -352,6 +379,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { context: { clientName: 'irys', }, + notifyOnNetworkStatusChange: true, }); const handleCommentChange = useCallback( @@ -360,31 +388,24 @@ const RequestElement = ({ request }: { request: RequestData }) => { ); const handleNewComment = useCallback(async () => { - // const comment = { - // owner: currentAddress, - // timestamp: (Date.now() / 1000).toString(), - // content: newComment, - // }; - const tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, { name: TAG_NAMES.operationName, value: 'Comment' }, { name: 'Comment-For', value: request.id }, - { name: 'Comment-Reply-To-Comment', value: request.id }, { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, ]; await postOnArweave(newComment, tags); - // setComments((prev) => [...prev, comment]); + refetch(); setNewComment(''); }, [request, newComment, currentAddress, setComments, setNewComment]); useEffect(() => { if (commentsData && commentsData.transactions.edges) { (async () => { - const allComments = []; + const allComments: Comment[] = []; setCommentsLoadingAnim(true); for (const tx of commentsData.transactions.edges) { const res = await fetch(`https://arweave.net/${tx.node.id}`); @@ -398,13 +419,63 @@ const RequestElement = ({ request }: { request: RequestData }) => { (tag: { name: string; value: string }) => tag.name === TAG_NAMES.unixTime, )?.value ?? '', content: data, + tags: tx.node.tags, }); } - setComments(allComments); - setTimeout(() => { - setCommentsLoadingAnim(false); - }, 5000); + const allCommentsWithReplies: Comment[] = []; + + // check and organize comment replies + allComments.forEach((comment: Comment) => { + if (allCommentsWithReplies.find((parent) => parent.id === comment.id)) { + // if this comment is already added, skip it + return; + } + const foundId = comment.tags.find( + (tag) => tag.name === 'Reply-Chain-Main-Parent-Id', + )?.value; + if (foundId) { + // its a reply + const foundParentIndex = allCommentsWithReplies.findIndex( + (parent) => parent.id === foundId, + ); + if (foundParentIndex >= 0) { + if (!allCommentsWithReplies[foundParentIndex]?.replies) { + allCommentsWithReplies[foundParentIndex].replies = []; + } + + allCommentsWithReplies[foundParentIndex].replies.push(comment); + } else { + const foundParentInAllComments = allComments.find( + (allComment) => allComment.id === foundId, + ); + if (foundParentInAllComments) { + allCommentsWithReplies.push({ + ...foundParentInAllComments, + ...{ + replies: [comment], + }, + }); + } + } + } else { + // if its reply and no parent was found, show it as a parent anyway + allCommentsWithReplies.push(comment); + } + }); + + const totalComments = allCommentsWithReplies.reduce((acc: number, comment: Comment) => { + if (comment.replies) { + acc += comment.replies.length + 1; + } else { + acc += 1; + } + return acc; + }, 0); + + setCommentsAmountTotal(totalComments); + setComments(allCommentsWithReplies); + setCommentsLoadingAnim(false); })(); } }, [commentsData, setComments]); @@ -531,9 +602,9 @@ const RequestElement = ({ request }: { request: RequestData }) => {
- {comments.length} {comments.length === 1 ? ' comment' : 'comments'} + {commentsAmountTotal} {commentsAmountTotal === 1 ? ' comment' : 'comments'}
- {comments.length === 0 && ( + {commentsAmountTotal === 0 && ( { {'No comments yet.'} )} - {comments.map((comment) => ( - - ))} + + {(loadingQuery || commentsLoadingAnim) && ( +
+ + Loading comments ... +
+ )} + + {!loadingQuery && !commentsLoadingAnim && ( + <> + {comments.map((comment) => ( +
+ + + {comment?.replies?.length && ( + <> + {comment.replies.map((reply) => ( +
+ tag.name === 'Replying-To-User-Address', + )?.value ?? '' + } + /> +
+ ))} + + )} +
+ ))} + + )} @@ -789,9 +901,14 @@ const RequestElement = ({ request }: { request: RequestData }) => { > } ${ - comments.length - } ${comments.length === 1 ? ' comment' : ' comments'}`} + icon={ + <> + {commentsLoadingAnim && } + + } + label={`${commentsAmountTotal} ${ + commentsAmountTotal === 1 ? ' comment' : ' comments' + }`} color='secondary' /> { const { data, loading } = useQuery(irysQuery, { variables: { tags: [ - { name: TAG_NAMES.protocolName, values: ['FairAI-test'] }, - { name: TAG_NAMES.protocolVersion, values: ['test'] }, + { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME_TEST] }, + { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION_TEST] }, { name: TAG_NAMES.operationName, values: ['Request-Solution'] }, ], first: 10, @@ -845,6 +962,7 @@ const BrowseRequests = () => { context: { clientName: 'irys', }, + notifyOnNetworkStatusChange: true, }); const isLoading = useMemo(() => loading || filtering, [loading, filtering]); From c0b68ff9e25722f114ee9137508cdf86704d6d25 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Thu, 17 Apr 2025 00:27:02 +0100 Subject: [PATCH 15/20] fix: fixed husky --- .husky/commit-msg | 1 - .husky/pre-commit | 1 - 2 files changed, 2 deletions(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index a5a6ecaa..c160a771 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -2,4 +2,3 @@ . "$(dirname -- "$0")/_/husky.sh" npx --no -- commitlint --edit ${1} -npx --no -- commitlint --edit ${1} diff --git a/.husky/pre-commit b/.husky/pre-commit index 87689ec6..1c3f56b4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,4 +3,3 @@ echo Checking lint... npx --no -- lint-staged -npx --no -- lint-staged From 72c3c7588e5e10f2f3a0101bfe0f4580e5c91449 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Mon, 21 Apr 2025 23:37:49 +0100 Subject: [PATCH 16/20] feat: added comment extra properties - budget suggestions --- client/src/pages/browse-requests.tsx | 169 ++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 15 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 5d417a6e..0f95cc61 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -46,6 +46,7 @@ import { FormControl, MenuItem, CircularProgress, + Switch, } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SendIcon from '@mui/icons-material/Send'; @@ -115,7 +116,9 @@ interface Comment { owner: string; timestamp: string; content: string; + commentType: 'text' | 'suggestion'; showReplyInput?: boolean; + showMakeSuggestionInputs?: boolean; replies?: Comment[]; tags: ITag[]; } @@ -134,7 +137,7 @@ const CommentElement = ({ isReply: boolean; replyToUserAddress: string; replyChainMainParentId: string; - commentType: 'text' | 'application'; + commentType: 'text' | 'suggestion'; refetchComments: () => void; }) => { const handleAddressClick = useCallback(() => { @@ -146,6 +149,24 @@ const CommentElement = ({ const [replyingToUserAddress, setReplyingToUserAddress] = useState(''); const [reply, setReply] = useState(''); + const handleShowSuggestionInputsReply = (commentToChange: Comment) => { + if (commentToChange.showReplyInput) { + // reply is open, open the suggestion inputs + commentToChange.showMakeSuggestionInputs = !commentToChange.showMakeSuggestionInputs; + } else { + // reply is closed + commentToChange.showMakeSuggestionInputs = false; + } + + if (commentToChange.showMakeSuggestionInputs) { + commentToChange.commentType = 'suggestion'; + } else { + commentToChange.commentType = 'text'; + } + + setChangedComment((prev) => ({ ...prev, ...commentToChange })); + }; + const handleSetShowAddReply = ( commentToChange: Comment, replyToCommentId: string, @@ -157,12 +178,13 @@ const CommentElement = ({ setReplyingToCommentId(''); setReplyingToUserAddress(''); } else { + // reply is closed, initiate new reply setReplyingToCommentId(replyToCommentId); setReplyingToUserAddress(replyToCommentOwnerAddress); } commentToChange.showReplyInput = !commentToChange.showReplyInput; - setChangedComment((prev) => ({ ...prev, reply: reply })); + setChangedComment((prev) => ({ ...prev, ...commentToChange })); }; const handlePostReplyToComment = async ( @@ -178,7 +200,7 @@ const CommentElement = ({ { name: 'Replying-To-Comment-Id', value: replyingToCommentId }, { name: 'Replying-To-User-Address', value: replyingToUserAddress }, { name: 'Reply-Chain-Main-Parent-Id', value: replyChainMainParentId }, - { name: 'Is-Suggestion', value: isSuggestion.toString() }, + { name: 'Comment-Type', value: isSuggestion ? 'suggestion' : 'text' }, { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, ]; @@ -196,7 +218,7 @@ const CommentElement = ({
@@ -205,7 +227,7 @@ const CommentElement = ({ )} - {commentType === 'application' && } + {commentType === 'suggestion' && } {!isReply ? 'Comment by ' : 'Reply by '} @@ -233,14 +255,14 @@ const CommentElement = ({
)} - {commentType === 'application' && ( + {commentType === 'suggestion' && (
- Application / Suggestion + Suggestion
)}
- {commentType === 'application' && ( + {commentType === 'suggestion' && (
@@ -308,13 +330,125 @@ const CommentElement = ({ {changedComment?.showReplyInput && ( <> -
- Replying to{' '} - - {replyingToUserAddress.slice(0, 6)}...{replyingToUserAddress.slice(-4)} - {' '} - : +
+
+ Replying to + + {replyingToUserAddress.slice(0, 6)}...{replyingToUserAddress.slice(-4)} + + : +
+ +
+ handleShowSuggestionInputsReply(changedComment)} + /> + Suggest different budgets +
+ + {changedComment?.showMakeSuggestionInputs && ( + <> +
+ + Suggesting new budgets and/or + targets + +

+ Fill any field as needed. All fields are optional.
+ These will be added to your comment and will be publicly visible. +

+
+
+ Budgets and date targets suggestion + +
+ + + + Daily deliveries and payments + Weekly deliveries and payments + Monthly deliveries and payments + Yearly deliveries and payments + All at once, right at the start + All at once, when project ends + + + +
+
+ + + + or +
+ + + Day(s) + Week(s) + Month(s) + Year(s) + +
+
+
+
+ + Ways to contact you + +
+
+ + @ }} + /> + @ }} + /> +
+
+ + Anything else? +
+ + )} +
{ allComments.push({ id: tx.node.id, owner: tx.node.address, + commentType: + tx.node.tags.find( + (tag: { name: string; value: string }) => tag.name === 'Comment-Type', + )?.value ?? 'text', timestamp: tx.node.tags.find( (tag: { name: string; value: string }) => tag.name === TAG_NAMES.unixTime, @@ -431,6 +569,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { // if this comment is already added, skip it return; } + // lets use the main parent Id for now ... (only 1 level of replies) const foundId = comment.tags.find( (tag) => tag.name === 'Reply-Chain-Main-Parent-Id', )?.value; From f3f12eee428a52fe0d1c84aea2fa83359c12c82b Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Wed, 23 Apr 2025 01:08:34 +0100 Subject: [PATCH 17/20] feat: request replies suggestions now work --- client/package.json | 2 +- client/src/pages/browse-requests.tsx | 335 ++++++++++++++++----------- 2 files changed, 202 insertions(+), 135 deletions(-) diff --git a/client/package.json b/client/package.json index 9c10a57d..50c66fde 100644 --- a/client/package.json +++ b/client/package.json @@ -54,7 +54,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-device-detect": "^2.2.3", "react-dom": "18.2.0", - "react-hook-form": "7.43.1", + "react-hook-form": "^7.43.1", "react-markdown": "^9.0.1", "react-number-format": "5.1.4", "react-router-dom": "6.8.1", diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 0f95cc61..d9bc3a56 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -74,6 +74,8 @@ import { IRequestSolution } from '@/interfaces/common'; import dayjs from 'dayjs'; import MakeRequestBanner from '@/components/make-request-banner'; import { ITag } from '@/interfaces/arweave'; +import { Controller, useForm } from 'react-hook-form'; +import moment from 'moment'; interface IrysTx { id: string; @@ -90,6 +92,28 @@ interface RequestData extends IRequestSolution { timestamp: string; } +interface FormCommentInputs { + budget: string; + paymentPlan: string; + dateTarget: string; + websiteUrl: string; + twitterHandle: string; + linkedinHandle: string; + commentText: string; +} + +interface Comment { + id: string; + owner: string; + timestamp: string; + content: string; + commentType: 'text' | 'suggestion'; + showReplyInput?: boolean; + replies?: Comment[]; + tags: ITag[]; + suggestionFields?: FormCommentInputs; +} + const irysQuery = gql` query requestsOnIrys($tags: [TagFilter!], $first: Int, $after: String) { transactions(tags: $tags, first: $first, after: $after, order: DESC) { @@ -111,18 +135,6 @@ const irysQuery = gql` } `; -interface Comment { - id: string; - owner: string; - timestamp: string; - content: string; - commentType: 'text' | 'suggestion'; - showReplyInput?: boolean; - showMakeSuggestionInputs?: boolean; - replies?: Comment[]; - tags: ITag[]; -} - const CommentElement = ({ comment, request, @@ -147,24 +159,21 @@ const CommentElement = ({ const [changedComment, setChangedComment] = useState(comment); const [replyingToCommentId, setReplyingToCommentId] = useState(''); const [replyingToUserAddress, setReplyingToUserAddress] = useState(''); - const [reply, setReply] = useState(''); - - const handleShowSuggestionInputsReply = (commentToChange: Comment) => { - if (commentToChange.showReplyInput) { - // reply is open, open the suggestion inputs - commentToChange.showMakeSuggestionInputs = !commentToChange.showMakeSuggestionInputs; - } else { - // reply is closed - commentToChange.showMakeSuggestionInputs = false; - } - - if (commentToChange.showMakeSuggestionInputs) { - commentToChange.commentType = 'suggestion'; - } else { - commentToChange.commentType = 'text'; - } + const [isSuggestion, setIsSuggestion] = useState(false); - setChangedComment((prev) => ({ ...prev, ...commentToChange })); + // declare comment form + const { + register, + handleSubmit, + reset: resetForm, + formState, + getValues, + control, + } = useForm(); + // const onReplySubmit: SubmitHandler = (data) => console.log(data); + + const handleShowSuggestionInputsReply = () => { + setIsSuggestion(!isSuggestion); }; const handleSetShowAddReply = ( @@ -172,9 +181,9 @@ const CommentElement = ({ replyToCommentId: string, replyToCommentOwnerAddress: string, ) => { - if (reply && commentToChange.showReplyInput) { + if (commentToChange.showReplyInput) { // reply is open, we are closing it now - setReply(''); // clear the reply input + resetForm(); // clear the reply input setReplyingToCommentId(''); setReplyingToUserAddress(''); } else { @@ -187,28 +196,46 @@ const CommentElement = ({ setChangedComment((prev) => ({ ...prev, ...commentToChange })); }; - const handlePostReplyToComment = async ( - commentToChange: Comment, - reply: string, - isSuggestion: boolean = false, - ) => { - const tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, - { name: TAG_NAMES.operationName, value: 'Comment' }, - { name: 'Comment-For', value: request.id }, - { name: 'Replying-To-Comment-Id', value: replyingToCommentId }, - { name: 'Replying-To-User-Address', value: replyingToUserAddress }, - { name: 'Reply-Chain-Main-Parent-Id', value: replyChainMainParentId }, - { name: 'Comment-Type', value: isSuggestion ? 'suggestion' : 'text' }, - { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, - ]; + const handlePostReplyToComment = async (commentToChange: Comment) => { + if (formState.isValid) { + let tags = [ + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, + { name: TAG_NAMES.operationName, value: 'Comment' }, + { name: 'Comment-For', value: request.id }, + { name: 'Replying-To-Comment-Id', value: replyingToCommentId }, + { name: 'Replying-To-User-Address', value: replyingToUserAddress }, + { name: 'Reply-Chain-Main-Parent-Id', value: replyChainMainParentId }, + { name: 'Comment-Type', value: isSuggestion ? 'suggestion' : 'text' }, + { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, + ]; + + if (isSuggestion) { + // declare tags of extra suggestion fields + tags = tags.concat([ + { + name: 'Suggestion-Budget', + value: getValues().budget, + }, + { name: 'Suggestion-PaymentPlan', value: getValues().paymentPlan }, + { + name: 'Suggestion-DateTargetISO', + value: getValues().dateTarget ? moment(getValues().dateTarget)?.toISOString() : '', + }, + { name: 'Suggestion-Website', value: getValues().websiteUrl }, + { name: 'Suggestion-TwitterXHandle', value: getValues().twitterHandle }, + { name: 'Suggestion-LinkedInHandle', value: getValues().linkedinHandle }, + ]); + } - await postOnArweave(reply, tags); + // submit data to blockchain + await postOnArweave(getValues().commentText, tags); - refetchComments(); + resetForm(); + handleSetShowAddReply(commentToChange, '', ''); - handleSetShowAddReply(commentToChange, '', ''); + refetchComments(); + } }; return ( @@ -266,49 +293,59 @@ const CommentElement = ({
- Budget suggestion: US$ 6,000.00 + Budget suggestion: {' '} + {changedComment.suggestionFields?.budget ?? '(Not Provided)'}
- Time suggestion: 6 month(s) + Date target: {' '} + {changedComment.suggestionFields?.dateTarget ?? '(Not Provided)'}
- Payment / Deliveries: All at once, right at the start + Payment / Deliveries: {' '} + {paymentPlanBrowseTag(changedComment.suggestionFields?.paymentPlan ?? '') ?? + '(Not Provided)'}
-
- X / Twitter: - - @getfairai - -
-
- LinkedIn: - - @getfairai - -
-
- Website: - - getfair.ai - -
+ {changedComment.suggestionFields?.twitterHandle && ( + + )} + {changedComment.suggestionFields?.linkedinHandle && ( + + )} + {changedComment.suggestionFields?.websiteUrl && ( + + )}
)} @@ -341,14 +378,14 @@ const CommentElement = ({
handleShowSuggestionInputsReply(changedComment)} + checked={isSuggestion} + onClick={() => handleShowSuggestionInputsReply()} /> Suggest different budgets
- {changedComment?.showMakeSuggestionInputs && ( + {isSuggestion && ( <>
@@ -364,57 +401,54 @@ const CommentElement = ({ Budgets and date targets suggestion
- + ( + field.onChange(budgetText)} + > + )} + /> + - Daily deliveries and payments - Weekly deliveries and payments - Monthly deliveries and payments - Yearly deliveries and payments - All at once, right at the start - All at once, when project ends + Daily deliveries and payments + Weekly deliveries and payments + Monthly deliveries and payments + Yearly deliveries and payments + + All at once, right at the start + + All at once, when project ends -
-
+ ( - + field.onChange(date)} + /> - or -
- - - Day(s) - Week(s) - Month(s) - Year(s) - -
-
-
+ )} + />
Ways to contact you @@ -426,6 +460,7 @@ const CommentElement = ({ placeholder='(optional)' variant='outlined' className='w-full max-w-[300px]' + {...register('websiteUrl', { maxLength: 50 })} /> @ }} + {...register('twitterHandle', { maxLength: 50 })} /> @ }} + {...register('linkedinHandle', { maxLength: 50 })} />
- Anything else? + Your comment
)} @@ -462,11 +499,10 @@ const CommentElement = ({ placeholder='Type your comment here' variant='outlined' fullWidth - value={reply} - onChange={(e) => setReply(e.target.value)} + {...register('commentText', { required: true, maxLength: 2000 })} /> handlePostReplyToComment(changedComment, reply, false)} + onClick={handleSubmit(() => handlePostReplyToComment(changedComment))} className='primary plausible-event-name=Request+Reply+Post+Click' > Send @@ -573,6 +609,37 @@ const RequestElement = ({ request }: { request: RequestData }) => { const foundId = comment.tags.find( (tag) => tag.name === 'Reply-Chain-Main-Parent-Id', )?.value; + + // set the comment type by tag + comment.commentType = + comment.tags.find((tag) => tag.name === 'Comment-Type')?.value === 'suggestion' + ? 'suggestion' + : 'text'; + + if (comment.commentType === 'suggestion') { + // set additional tag info + comment.suggestionFields = { + budget: comment.tags.find((tag) => tag.name === 'Suggestion-Budget')?.value ?? '', + paymentPlan: + comment.tags.find((tag) => tag.name === 'Suggestion-PaymentPlan')?.value ?? '', + dateTarget: + comment.tags.find((tag) => tag.name === 'Suggestion-DateTargetISO')?.value ?? '', + websiteUrl: + comment.tags.find((tag) => tag.name === 'Suggestion-Website')?.value ?? '', + twitterHandle: + comment.tags.find((tag) => tag.name === 'Suggestion-TwitterXHandle')?.value ?? '', + linkedinHandle: + comment.tags.find((tag) => tag.name === 'Suggestion-LinkedInHandle')?.value ?? '', + commentText: '', // not used here + }; + + if (comment.suggestionFields.dateTarget) { + comment.suggestionFields.dateTarget = moment(comment.suggestionFields.dateTarget) + .toDate() + .toDateString(); + } + } + if (foundId) { // its a reply const foundParentIndex = allCommentsWithReplies.findIndex( @@ -772,7 +839,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { comment={comment} request={request} refetchComments={refetch} - commentType='text' + commentType={comment.commentType} replyToUserAddress='' replyChainMainParentId={comment.id} /> @@ -787,7 +854,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { comment={reply} request={request} refetchComments={refetch} - commentType='text' + commentType={reply.commentType} replyToUserAddress={ reply.tags.find( (tag) => tag.name === 'Replying-To-User-Address', From bde9092247674a5295c20bdc0b158b615cf1fbda Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Mon, 28 Apr 2025 17:22:50 +0100 Subject: [PATCH 18/20] feat: sync changes --- client/src/pages/browse-requests.tsx | 324 +++++++++++++++------------ 1 file changed, 180 insertions(+), 144 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index d9bc3a56..4ba82897 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -169,13 +169,26 @@ const CommentElement = ({ formState, getValues, control, + trigger, } = useForm(); - // const onReplySubmit: SubmitHandler = (data) => console.log(data); const handleShowSuggestionInputsReply = () => { setIsSuggestion(!isSuggestion); }; + const checkIfAtLeastOneFieldIsFilled = (): boolean => { + const fieldValues = Object.values(getValues()).filter( + (value) => value && value !== getValues().commentText, + ); + if (fieldValues.length === 0) { + return false; + } else return true; + }; + + useEffect(() => { + trigger(); + }, [isSuggestion]); + const handleSetShowAddReply = ( commentToChange: Comment, replyToCommentId: string, @@ -198,6 +211,10 @@ const CommentElement = ({ const handlePostReplyToComment = async (commentToChange: Comment) => { if (formState.isValid) { + if (isSuggestion && !checkIfAtLeastOneFieldIsFilled()) { + setIsSuggestion(false); + } + let tags = [ { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, @@ -504,6 +521,7 @@ const CommentElement = ({ handlePostReplyToComment(changedComment))} className='primary plausible-event-name=Request+Reply+Post+Click' + disabled={!formState.isValid} > Send @@ -523,14 +541,15 @@ const RequestElement = ({ request }: { request: RequestData }) => { const [commentsAmountTotal, setCommentsAmountTotal] = useState(0); const [newComment, setNewComment] = useState(''); const [showAddComment, setShowAddComment] = useState(false); - const [showAcceptAsDev, setShowAcceptAsDev] = useState(false); + const [showExtraSuggestionOptions, setShowExtraSuggestionOptions] = useState(false); const { currentAddress } = useContext(EVMWalletContext); const handleOpen = useCallback(() => setOpen(true), [setOpen]); const handleClose = useCallback(() => setOpen(false), [setOpen]); const handleShowNewComment = () => setShowAddComment(!showAddComment); - const handleShowAcceptAsDev = () => setShowAcceptAsDev(!showAcceptAsDev); + const handleShowExtraSuggestionOptions = () => + setShowExtraSuggestionOptions(!showExtraSuggestionOptions); const { data: commentsData, @@ -880,7 +899,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { animate={{ opacity: 1, y: 0, transition: { delay: 0.1 } }} >
- {!showAddComment && !showAcceptAsDev && ( + {!showAddComment && (
@@ -888,13 +907,164 @@ const RequestElement = ({ request }: { request: RequestData }) => { Add a comment +
+ )} + + {showExtraSuggestionOptions && ( + <> +
+
+ Type your comment +
+ +
+ handleShowSuggestionInputsReply()} + /> + Suggest different budgets +
+
+ + {isSuggestion && ( + <> +
+ + Suggesting new + budgets and/or targets + +

+ Fill any field as needed. All fields are optional.
+ These will be added to your comment and will be publicly visible. +

+
+
+ Budgets and date targets suggestion + +
+ ( + field.onChange(budgetText)} + > + )} + /> - - - I am interested in developing this request + + + + Daily deliveries and payments + + + Weekly deliveries and payments + + + Monthly deliveries and payments + + + Yearly deliveries and payments + + + All at once, right at the start + + + All at once, when project ends + + + + + ( + + field.onChange(date)} + /> + + )} + /> +
+ + Ways to contact you + +
+
+ + @ }} + {...register('twitterHandle', { maxLength: 50 })} + /> + @ }} + {...register('linkedinHandle', { maxLength: 50 })} + /> +
+
+ + Your comment +
+ + )} + +
+ + handleSetShowAddReply( + changedComment, + changedComment.id, + changedComment.owner, + ) + } + > + - -
+ + handlePostReplyToComment(changedComment))} + className='primary plausible-event-name=Request+Reply+Post+Click' + disabled={!formState.isValid} + > + Send + +
+ )} {showAddComment && ( @@ -916,144 +1086,10 @@ const RequestElement = ({ request }: { request: RequestData }) => { onClick={handleNewComment} className='primary plausible-event-name=Request+Comment+Click' > - Submit + Send
)} - - {showAcceptAsDev && ( -
- - Request to be a developer - for this request - - - You are about to suggest yourself as a developer or one of the - developers for this request. -
- Everything you fill here will be publicly visible in this request - comments! -
- You can accept everything as is, or suggest a different budget, - different target date, time needed or a different payment plan. -
- The request creator will be able to make a response to your request and - let you know more details. -
You can also leave any additional info or comments in the - "Anything else?" box, as needed. -
- - You suggestion / proposal - -
- - } - label='I accept the request as is and am ready to start right away.' - /> - -
- - Budgets and date targets suggestion - -
- - - - Daily deliveries and payments - Weekly deliveries and payments - Monthly deliveries and payments - Yearly deliveries and payments - All at once, right at the start - All at once, when project ends - - - -
-
- - - - or -
- - - Day(s) - Week(s) - Month(s) - Year(s) - -
-
-
-
- - Ways to contact you - -
- - @ }} - /> - @ }} - /> -
- - Anything else? - - - -
- - Cancel - - - Submit - -
-
- )}
)} From 5f84e446386dd325d621584ca1e3aeb2e69159b4 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:43:59 +0100 Subject: [PATCH 19/20] feat: finished dev on requests comments --- client/src/pages/browse-requests.tsx | 173 +++++++++++++++------------ 1 file changed, 98 insertions(+), 75 deletions(-) diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx index 4ba82897..2977ebc3 100644 --- a/client/src/pages/browse-requests.tsx +++ b/client/src/pages/browse-requests.tsx @@ -22,7 +22,7 @@ import { paymentPlanBrowseTag, paymentPlanType, } from '@/utils/requestsPipeFunctions'; -import { PROTOCOL_NAME_TEST, PROTOCOL_VERSION_TEST, TAG_NAMES } from '@/constants'; +import { PROTOCOL_NAME, PROTOCOL_VERSION, TAG_NAMES } from '@/constants'; import { gql, useQuery } from '@apollo/client'; import Close from '@mui/icons-material/Close'; import { @@ -40,9 +40,6 @@ import { Fab, Tooltip, useTheme, - FormGroup, - FormControlLabel, - Checkbox, FormControl, MenuItem, CircularProgress, @@ -50,7 +47,7 @@ import { } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SendIcon from '@mui/icons-material/Send'; -import { ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { EVMWalletContext } from '@/context/evm-wallet'; import { postOnArweave } from '@fairai/evm-sdk'; import { motion } from 'framer-motion'; @@ -59,9 +56,7 @@ import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRound import useScroll from '@/hooks/useScroll'; import useWindowDimensions from '@/hooks/useWindowDimensions'; import { - AddRounded, ChatBubbleRounded, - CheckCircleRounded, CloseRounded, InfoRounded, ReplyRounded, @@ -156,6 +151,7 @@ const CommentElement = ({ window.open(`https://arbiscan.io/address/${comment.owner}`, '_blank'); }, [comment]); + const { currentAddress } = useContext(EVMWalletContext); const [changedComment, setChangedComment] = useState(comment); const [replyingToCommentId, setReplyingToCommentId] = useState(''); const [replyingToUserAddress, setReplyingToUserAddress] = useState(''); @@ -216,8 +212,8 @@ const CommentElement = ({ } let tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, { name: TAG_NAMES.operationName, value: 'Comment' }, { name: 'Comment-For', value: request.id }, { name: 'Replying-To-Comment-Id', value: replyingToCommentId }, @@ -369,7 +365,7 @@ const CommentElement = ({
{changedComment.content}
- {!changedComment?.showReplyInput && ( + {currentAddress && !changedComment?.showReplyInput && (
)} - {changedComment?.showReplyInput && ( + {currentAddress && changedComment?.showReplyInput && ( <>
@@ -398,7 +394,7 @@ const CommentElement = ({ checked={isSuggestion} onClick={() => handleShowSuggestionInputsReply()} /> - Suggest different budgets + Suggest budgets
@@ -539,17 +535,51 @@ const RequestElement = ({ request }: { request: RequestData }) => { const [comments, setComments] = useState([]); const [commentsLoadingAnim, setCommentsLoadingAnim] = useState(false); const [commentsAmountTotal, setCommentsAmountTotal] = useState(0); - const [newComment, setNewComment] = useState(''); const [showAddComment, setShowAddComment] = useState(false); - const [showExtraSuggestionOptions, setShowExtraSuggestionOptions] = useState(false); const { currentAddress } = useContext(EVMWalletContext); const handleOpen = useCallback(() => setOpen(true), [setOpen]); const handleClose = useCallback(() => setOpen(false), [setOpen]); - const handleShowNewComment = () => setShowAddComment(!showAddComment); - const handleShowExtraSuggestionOptions = () => - setShowExtraSuggestionOptions(!showExtraSuggestionOptions); + const handleShowNewComment = () => { + setShowAddComment(!showAddComment); + + // reset this if not showing new comment + if (!showAddComment) { + setIsSuggestion(false); + resetForm(); + } + }; + + const [isSuggestion, setIsSuggestion] = useState(false); + + // declare comment form + const { + register, + handleSubmit, + reset: resetForm, + formState, + getValues, + control, + trigger, + } = useForm(); + + const handleShowSuggestionInputsReply = () => { + setIsSuggestion(!isSuggestion); + }; + + const checkIfAtLeastOneFieldIsFilled = (): boolean => { + const fieldValues = Object.values(getValues()).filter( + (value) => value && value !== getValues().commentText, + ); + if (fieldValues.length === 0) { + return false; + } else return true; + }; + + useEffect(() => { + trigger(); + }, [isSuggestion]); const { data: commentsData, @@ -558,8 +588,8 @@ const RequestElement = ({ request }: { request: RequestData }) => { } = useQuery(irysQuery, { variables: { tags: [ - { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME_TEST] }, - { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION_TEST] }, + { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME] }, + { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION] }, { name: TAG_NAMES.operationName, values: ['Comment'] }, { name: 'Comment-For', values: [request.id] }, ], @@ -571,25 +601,48 @@ const RequestElement = ({ request }: { request: RequestData }) => { notifyOnNetworkStatusChange: true, }); - const handleCommentChange = useCallback( - (e: ChangeEvent) => setNewComment(e.target.value), - [setNewComment], - ); + const handlePostNewComment = async () => { + if (formState.isValid) { + if (isSuggestion && !checkIfAtLeastOneFieldIsFilled()) { + setIsSuggestion(false); + } + + let tags = [ + { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME }, + { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION }, + { name: TAG_NAMES.operationName, value: 'Comment' }, + { name: 'Comment-For', value: request.id }, + { name: 'Comment-Type', value: isSuggestion ? 'suggestion' : 'text' }, + { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, + ]; + + if (isSuggestion) { + // declare tags of extra suggestion fields + tags = tags.concat([ + { + name: 'Suggestion-Budget', + value: getValues().budget, + }, + { name: 'Suggestion-PaymentPlan', value: getValues().paymentPlan }, + { + name: 'Suggestion-DateTargetISO', + value: getValues().dateTarget ? moment(getValues().dateTarget)?.toISOString() : '', + }, + { name: 'Suggestion-Website', value: getValues().websiteUrl }, + { name: 'Suggestion-TwitterXHandle', value: getValues().twitterHandle }, + { name: 'Suggestion-LinkedInHandle', value: getValues().linkedinHandle }, + ]); + } - const handleNewComment = useCallback(async () => { - const tags = [ - { name: TAG_NAMES.protocolName, value: PROTOCOL_NAME_TEST }, - { name: TAG_NAMES.protocolVersion, value: PROTOCOL_VERSION_TEST }, - { name: TAG_NAMES.operationName, value: 'Comment' }, - { name: 'Comment-For', value: request.id }, - { name: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() }, - ]; + // submit data to blockchain + await postOnArweave(getValues().commentText, tags); - await postOnArweave(newComment, tags); + resetForm(); + handleShowNewComment(); - refetch(); - setNewComment(''); - }, [request, newComment, currentAddress, setComments, setNewComment]); + refetch(); + } + }; useEffect(() => { if (commentsData && commentsData.transactions.edges) { @@ -910,11 +963,11 @@ const RequestElement = ({ request }: { request: RequestData }) => {
)} - {showExtraSuggestionOptions && ( + {showAddComment && ( <>
- Type your comment + {isSuggestion ? '' : 'Type your comment'}
@@ -922,7 +975,7 @@ const RequestElement = ({ request }: { request: RequestData }) => { checked={isSuggestion} onClick={() => handleShowSuggestionInputsReply()} /> - Suggest different budgets + Suggest budgets
@@ -1037,28 +1090,22 @@ const RequestElement = ({ request }: { request: RequestData }) => { )} -
+
- handleSetShowAddReply( - changedComment, - changedComment.id, - changedComment.owner, - ) - } + onClick={handleShowNewComment} > handlePostReplyToComment(changedComment))} - className='primary plausible-event-name=Request+Reply+Post+Click' + onClick={handleSubmit(() => handlePostNewComment())} + className='primary plausible-event-name=Request+Comment+Click' disabled={!formState.isValid} > Send @@ -1066,30 +1113,6 @@ const RequestElement = ({ request }: { request: RequestData }) => {
)} - - {showAddComment && ( -
- - - - - - Send - -
- )}
)} @@ -1195,8 +1218,8 @@ const BrowseRequests = () => { const { data, loading } = useQuery(irysQuery, { variables: { tags: [ - { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME_TEST] }, - { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION_TEST] }, + { name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME] }, + { name: TAG_NAMES.protocolVersion, values: [PROTOCOL_VERSION] }, { name: TAG_NAMES.operationName, values: ['Request-Solution'] }, ], first: 10, From c2e8e2f8b804fd38b7740d5544eb6cd26c257043 Mon Sep 17 00:00:00 2001 From: azenyr <61697668+azenyr@users.noreply.github.com> Date: Tue, 13 May 2025 11:55:21 +0100 Subject: [PATCH 20/20] feat: update FairAI logos --- .../logo_non_capitalized_black_transp.svg | 85 ++++++++++++ .../logo_non_capitalized_white_transp.svg | 130 ++++++++++++++++++ client/src/components/logo.tsx | 2 +- client/src/components/navbar.tsx | 2 +- client/src/pages/sign-in.tsx | 2 +- 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 client/public/logo_non_capitalized_black_transp.svg create mode 100644 client/public/logo_non_capitalized_white_transp.svg diff --git a/client/public/logo_non_capitalized_black_transp.svg b/client/public/logo_non_capitalized_black_transp.svg new file mode 100644 index 00000000..241b81ed --- /dev/null +++ b/client/public/logo_non_capitalized_black_transp.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/logo_non_capitalized_white_transp.svg b/client/public/logo_non_capitalized_white_transp.svg new file mode 100644 index 00000000..02bfcd56 --- /dev/null +++ b/client/public/logo_non_capitalized_white_transp.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/components/logo.tsx b/client/src/components/logo.tsx index 38076ccb..d2744082 100644 --- a/client/src/components/logo.tsx +++ b/client/src/components/logo.tsx @@ -21,7 +21,7 @@ import { Icon } from '@mui/material'; const Logo = () => { return ( - FairAI Logo + FairAI Logo ); }; diff --git a/client/src/components/navbar.tsx b/client/src/components/navbar.tsx index e8f938c7..88b61e91 100644 --- a/client/src/components/navbar.tsx +++ b/client/src/components/navbar.tsx @@ -351,7 +351,7 @@ const Navbar = ({ userScrolledDown }: { userScrolledDown: boolean }) => {
FairAI Logo {
FairAI Logo