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/public/fairai_logo_whitebg.png b/client/public/fairai_logo_whitebg.png
new file mode 100644
index 00000000..9a5e3855
Binary files /dev/null and b/client/public/fairai_logo_whitebg.png differ
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/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',
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 (
-
+
);
};
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/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 }) => {
{
`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/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..03e152cf 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: string;
+ paymentPlan: string;
+ targetUnixTimestamp: number;
+}
diff --git a/client/src/pages/browse-requests.tsx b/client/src/pages/browse-requests.tsx
index a7e559a0..2977ebc3 100644
--- a/client/src/pages/browse-requests.tsx
+++ b/client/src/pages/browse-requests.tsx
@@ -16,7 +16,12 @@
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-import DebounceIconButton from '@/components/debounce-icon-button';
+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';
@@ -34,16 +39,38 @@ import {
TextField,
Fab,
Tooltip,
+ useTheme,
+ FormControl,
+ MenuItem,
+ CircularProgress,
+ Switch,
} 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';
import { StyledMuiButton } from '@/styles/components';
import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded';
import useScroll from '@/hooks/useScroll';
+import useWindowDimensions from '@/hooks/useWindowDimensions';
+import {
+ ChatBubbleRounded,
+ CloseRounded,
+ InfoRounded,
+ ReplyRounded,
+ StarRounded,
+} from '@mui/icons-material';
+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';
+import { ITag } from '@/interfaces/arweave';
+import { Controller, useForm } from 'react-hook-form';
+import moment from 'moment';
interface IrysTx {
id: string;
@@ -54,13 +81,32 @@ interface IrysTx {
address: string;
}
-interface RequestData {
- title: string;
- description: string;
- keywords: string[];
+interface RequestData extends IRequestSolution {
+ id: string;
+ owner: string;
+ 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`
@@ -84,41 +130,402 @@ const irysQuery = gql`
}
`;
-interface Comment {
- owner: string;
- timestamp: string;
- content: string;
-}
-
-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' | 'suggestion';
+ refetchComments: () => void;
+}) => {
const handleAddressClick = useCallback(() => {
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('');
+ 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 handleSetShowAddReply = (
+ commentToChange: Comment,
+ replyToCommentId: string,
+ replyToCommentOwnerAddress: string,
+ ) => {
+ if (commentToChange.showReplyInput) {
+ // reply is open, we are closing it now
+ resetForm(); // clear the reply input
+ setReplyingToCommentId('');
+ setReplyingToUserAddress('');
+ } else {
+ // reply is closed, initiate new reply
+ setReplyingToCommentId(replyToCommentId);
+ setReplyingToUserAddress(replyToCommentOwnerAddress);
+ }
+
+ commentToChange.showReplyInput = !commentToChange.showReplyInput;
+ setChangedComment((prev) => ({ ...prev, ...commentToChange }));
+ };
+
+ const handlePostReplyToComment = async (commentToChange: Comment) => {
+ 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: '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 },
+ ]);
+ }
+
+ // submit data to blockchain
+ await postOnArweave(getValues().commentText, tags);
+
+ resetForm();
+ handleSetShowAddReply(commentToChange, '', '');
+
+ refetchComments();
+ }
+ };
+
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
+
+ {isReply &&
}
+
+
+
+
+
+ {commentType === 'suggestion' && (
+
+
+
+ Budget suggestion: {' '}
+ {changedComment.suggestionFields?.budget ?? '(Not Provided)'}
+
+
+ Date target: {' '}
+ {changedComment.suggestionFields?.dateTarget ?? '(Not Provided)'}
+
+
+ Payment / Deliveries: {' '}
+ {paymentPlanBrowseTag(changedComment.suggestionFields?.paymentPlan ?? '') ??
+ '(Not Provided)'}
+
+
+
+ {changedComment.suggestionFields?.twitterHandle && (
+
+ )}
+ {changedComment.suggestionFields?.linkedinHandle && (
+
+ )}
+ {changedComment.suggestionFields?.websiteUrl && (
+
+ )}
+
+
+ )}
+
+
{changedComment.content}
+
+ {currentAddress && !changedComment?.showReplyInput && (
+
+
+ handleSetShowAddReply(changedComment, changedComment.id, changedComment.owner)
+ }
+ >
+ Reply
+
+
+ )}
+
+ {currentAddress && changedComment?.showReplyInput && (
+ <>
+
+
+ Replying to
+
+ {replyingToUserAddress.slice(0, 6)}...{replyingToUserAddress.slice(-4)}
+
+ :
+
+
+
+ handleShowSuggestionInputsReply()}
+ />
+ Suggest 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)}
+ >
+ )}
+ />
+
+
+
+ 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
+
-
+ >
)}
-
{comment.content}
-
+
);
};
@@ -126,13 +533,59 @@ const CommentElement = ({ comment, request }: { comment: Comment; request: Reque
const RequestElement = ({ request }: { request: RequestData }) => {
const [open, setOpen] = useState(false);
const [comments, setComments] = useState
([]);
- const [newComment, setNewComment] = useState('');
+ const [commentsLoadingAnim, setCommentsLoadingAnim] = useState(false);
+ const [commentsAmountTotal, setCommentsAmountTotal] = useState(0);
+ const [showAddComment, setShowAddComment] = useState(false);
const { currentAddress } = useContext(EVMWalletContext);
const handleOpen = useCallback(() => setOpen(true), [setOpen]);
const handleClose = useCallback(() => setOpen(false), [setOpen]);
- const { data: commentsData } = useQuery(irysQuery, {
+ 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,
+ refetch,
+ loading: loadingQuery,
+ } = useQuery(irysQuery, {
variables: {
tags: [
{ name: TAG_NAMES.protocolName, values: [PROTOCOL_NAME] },
@@ -145,53 +598,162 @@ const RequestElement = ({ request }: { request: RequestData }) => {
context: {
clientName: 'irys',
},
+ notifyOnNetworkStatusChange: true,
});
- const handleCommentChange = useCallback(
- (e: ChangeEvent) => setNewComment(e.target.value),
- [setNewComment],
- );
+ const handlePostNewComment = async () => {
+ if (formState.isValid) {
+ if (isSuggestion && !checkIfAtLeastOneFieldIsFilled()) {
+ setIsSuggestion(false);
+ }
- const handleNewComment = useCallback(async () => {
- const comment = {
- owner: currentAddress,
- timestamp: (Date.now() / 1000).toString(),
- content: newComment,
- };
+ 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 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: TAG_NAMES.unixTime, value: (Date.now() / 1000).toString() },
- ];
+ // submit data to blockchain
+ await postOnArweave(getValues().commentText, tags);
- await postOnArweave(newComment, tags);
+ resetForm();
+ handleShowNewComment();
- setComments((prev) => [...prev, comment]);
- setNewComment('');
- }, [request, newComment, currentAddress, setComments, setNewComment]);
+ refetch();
+ }
+ };
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}`);
const data = await res.text();
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,
)?.value ?? '',
content: data,
+ tags: tx.node.tags,
});
}
- setComments(allComments);
+ 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;
+ }
+ // 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;
+
+ // 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(
+ (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]);
@@ -233,7 +795,7 @@ const RequestElement = ({ request }: { request: RequestData }) => {
animate={{ opacity: 1, y: 0 }}
className='w-full'
>
-
+
@@ -263,8 +825,14 @@ const RequestElement = ({ request }: { request: RequestData }) => {
-
-
+
+
{request.description}
{
label={keyword}
variant='filled'
color='primary'
- className='font-bold saturate-50'
+ className='font-bold saturate-50 brightness-105'
/>
))}
+
+
+ Request details
+
+
+
+ Planned budget:
+ {request.budget}
+
+
+ Payment / Deliveries:
+ {paymentPlanType(request.paymentPlan)}
+
+
+ Date target:
+ {dayjs(request.targetUnixTimestamp * 1000).format('MM/YYYY')}
+
+
+ Application Development:
+ {needsAppConfig(request.needsApp)}
+
+
+ Database:
+ {databaseConfigType(request.needsDb)}
+
+
+
+
- {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 ?? ''
+ }
+ />
+
+ ))}
+ >
+ )}
+
+ ))}
+ >
+ )}
@@ -315,38 +952,167 @@ const RequestElement = ({ request }: { request: RequestData }) => {
animate={{ opacity: 1, y: 0, transition: { delay: 0.1 } }}
>
-
+
+
+
+ Add a comment
+
+
+
+ )}
+
+ {showAddComment && (
+ <>
+
+
+ {isSuggestion ? '' : 'Type your comment'}
+
+
+
+ handleShowSuggestionInputsReply()}
+ />
+ Suggest 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)}
+ >
+ )}
+ />
+
+
+
+
+ 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
+
+ >
+ )}
+
+
+
-
-
- ),
- }}
- />
+
+
+
+ handlePostNewComment())}
+ className='primary plausible-event-name=Request+Comment+Click'
+ disabled={!formState.isValid}
+ >
+ Send
+
+
+ >
+ )}
)}
@@ -363,7 +1129,7 @@ const RequestElement = ({ request }: { request: RequestData }) => {
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 +1145,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 ? ' (...)' : '')}
{
gap={'5px'}
flexWrap={'wrap'}
>
-
- {comments.length} {comments.length === 1 ? ' comment' : ' comments'}
-
+
+
+ {commentsLoadingAnim && }
+ >
+ }
+ label={`${commentsAmountTotal} ${
+ commentsAmountTotal === 1 ? ' comment' : ' comments'
+ }`}
+ color='secondary'
+ />
+
+
{request.keywords.map((keyword) => (
))}
@@ -418,6 +1206,14 @@ const BrowseRequests = () => {
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: {
@@ -431,6 +1227,7 @@ const BrowseRequests = () => {
context: {
clientName: 'irys',
},
+ notifyOnNetworkStatusChange: true,
});
const isLoading = useMemo(() => loading || filtering, [loading, filtering]);
@@ -479,17 +1276,19 @@ const BrowseRequests = () => {
}, []); // run only on first load
return (
- <>
-
-
-
+
-
@@ -514,6 +1308,8 @@ const BrowseRequests = () => {
)}
+ {!isLoading &&
}
+
{requests.length === 0 && !isLoading && (
@@ -541,7 +1337,7 @@ const BrowseRequests = () => {
)}
-
+
{
- >
+
);
};
diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx
index d02e5e0e..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,
@@ -260,10 +261,10 @@ export default function Home() {
maxWidth: '100%',
},
mt: '40px',
- paddingBottom: '86px',
+ paddingBottom: '20px',
}}
>
-
+
Choose a solution and start creating
@@ -343,6 +344,12 @@ export default function Home() {
)}
+ {!error && !loading && (
+
+
+
+ )}
+
{!error && !loading && (
();
const Keyword = ({
currentKeyword,
@@ -35,11 +86,7 @@ const Keyword = ({
}: {
currentKeyword: string;
keywords: string[];
- setValue: UseFormSetValue<{
- title: string;
- description: string;
- keywords: string[];
- }>;
+ setValue: UseFormSetValue;
}) => {
const onDelete = useCallback(() => {
setValue(
@@ -48,25 +95,36 @@ const Keyword = ({
);
}, [currentKeyword, keywords, setValue]);
- return ;
+ return (
+
+ );
};
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: [],
+ needsDb: '',
+ needsApp: '',
+ budget: '',
+ paymentPlan: '',
+ targetUnixTimestamp: 0,
},
});
const { field: title } = useController({ control, name: 'title', rules: { required: true } });
@@ -75,15 +133,36 @@ 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.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 },
+ { 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() },
];
@@ -102,22 +181,51 @@ const RequestSolution = () => {
}
};
- const keyDownHandler = useCallback(
- (event: React.KeyboardEvent) => {
- if (event.code === 'Enter') {
- event.preventDefault();
- setValue('keywords', [...keywords, newKeyword]);
- setNewKeyword('');
+ const handleNewKeywordAutoCompleteChanged = useCallback(
+ (_: 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(
+ (_: 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);
+ }
+ },
+ [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));
},
- [setNewKeyword],
+ [filter, keywords],
);
if (requestSuccessful) {
@@ -131,7 +239,7 @@ const RequestSolution = () => {
alignItems={'center'}
overflow={'auto'}
>
- Your Request Has been registered.
+ Your Request Has Been Registered.
Thank you for your collaboration.
Explore Marketplace
@@ -141,121 +249,283 @@ const RequestSolution = () => {
}
return (
-
-
-
-
-
-
-
-
-
-
Tell us about your needs
+
+
+
+
+
+
Create a request: tell us 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.
-
-
-
- Please provide a detailed description of your problem
+
+
+
+ 2. Provide a detailed description
+
+
+ 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.
-
-
-
- Add a few keywords
-
-
- {keywords.map((keyword) => (
-
- ))}
-
- ),
+ sx={{
+ backgroundColor: 'white',
+ overflow: 'hidden',
+ borderRadius: '8px',
+ margin: '0px 16px',
+ maxWidth: '850px',
}}
/>
-
-
-
+
+
+
+ 3. Add keywords (categories) that best suit your request
+
+
+ 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. 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.'
+ />
+ }
+ label='This project does not need a database or specific data.'
+ />
+
+
+
+
+
+
+
+ 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, 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.'
+ />
+
+
+
+
+
+
+
+ 7. What is your expected budget for this project?
+
+
+ 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.
+
+
+
+
+
+
+
+
+ 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
+ project. 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
+
+
+
+
+
+
+
+ 9. What is your initial date target for this project?
+
+
+ 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
+ project. This is only a theoretical target, and can always be further discussed with the
+ developers.
+
+
+
+
+
+
Cancel
@@ -265,11 +535,11 @@ const RequestSolution = () => {
disabled={!formState.isDirty || !formState.isValid || formState.isSubmitted}
>
- Save and Submit
+ Submit Request
-
-
-
+
+
+
);
};
diff --git a/client/src/pages/sign-in.tsx b/client/src/pages/sign-in.tsx
index f9145f28..515bd161 100644
--- a/client/src/pages/sign-in.tsx
+++ b/client/src/pages/sign-in.tsx
@@ -107,7 +107,7 @@ const WalletnotConnectedContent = () => {
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 372fbccd..6c62bfd8 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 }) => ({
@@ -164,9 +164,14 @@ export const StyledMuiButton = styled('button')(({ 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': {
@@ -235,3 +240,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..75f2505f 100644
--- a/client/src/styles/main.css
+++ b/client/src/styles/main.css
@@ -164,3 +164,51 @@ html {
background-position: 86% 0%;
}
}
+
+@keyframes fadeInSlideDown {
+ 0% {
+ opacity: 0;
+ transform: translateY(-40px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+}
+
+@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;
+}
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;
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)';
+}