diff --git a/package.json b/package.json index a58868ed..be77a95d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/react-slick": "^0.23.10", "@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/parser": "^5.59.11", + "apexcharts": "^3.44.0", "axios": "^1.5.1", "bad-words": "^3.0.4", "bcryptjs": "^2.4.3", @@ -71,6 +72,7 @@ "rc-pagination": "^3.4.2", "react": "18.2.0", "react-activity-feed": "^1.4.0", + "react-apexcharts": "^1.4.1", "react-copy-to-clipboard": "^5.1.0", "react-country-flag": "^3.1.0", "react-country-region-selector": "^3.6.1", @@ -90,6 +92,7 @@ "stream-chat-react": "^10.7.3", "styled-components": "^5.3.9", "swagger-ui-react": "^4.19.0", + "swiper": "^11.0.3", "test-utils": "^1.1.1", "typescript": "5.0.2", "uuidv4": "^6.2.12" diff --git a/public/peer-to-peer.png b/public/peer-to-peer.png new file mode 100644 index 00000000..68b24e89 Binary files /dev/null and b/public/peer-to-peer.png differ diff --git a/src/components/AreaGraph/index.tsx b/src/components/AreaGraph/index.tsx new file mode 100644 index 00000000..76461928 --- /dev/null +++ b/src/components/AreaGraph/index.tsx @@ -0,0 +1,36 @@ +import dynamic from 'next/dynamic'; + +const Chart = dynamic(() => import('react-apexcharts'), { + ssr: false, +}); + +export default function AreaGrah() { + const options = { + fill: { + colors: ['#3B27C1'], + }, + stroke: { + height: '1px', + colors: ['#3B27C1'], + }, + dataLabels: { + enabled: false, + }, + toolbar: { + enabled: false, + show: false, + }, + xaxis: { + categories: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + }, + }; + + const series = [ + { + name: 'profile views', + data: [0, 0, 3, 5, 2, 6, 0], + }, + ]; + + return ; +} diff --git a/src/components/BriefComponent/index.tsx b/src/components/BriefComponent/index.tsx new file mode 100644 index 00000000..c9e07997 --- /dev/null +++ b/src/components/BriefComponent/index.tsx @@ -0,0 +1,150 @@ +import StarIcon from '@mui/icons-material/Star'; +import { Rating } from '@mui/material'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en.json'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { + AiOutlineCalendar, + AiOutlineClockCircle, + AiOutlinePlus, +} from 'react-icons/ai'; +import { HiOutlineCurrencyDollar } from 'react-icons/hi'; +import { TbNorthStar } from 'react-icons/tb'; +import { TbUsers } from 'react-icons/tb'; +import { TfiEmail } from 'react-icons/tfi'; +import { VscVerified } from 'react-icons/vsc'; + +import { Brief } from '@/model'; + +TimeAgo.addLocale(en); +const timeAgo = new TimeAgo('en-US'); + +export default function BriefComponent({ brief }: { brief: Brief }) { + const router = useRouter(); + return ( +
+
router.push(`/briefs/${brief.id}`)} + className='py-9 px-7 max-w-[70%] w-full' + > +

{brief.headline}

+
+

+ + {brief.experience_level} +

+

+ + Posted {timeAgo.format(new Date(brief.created))} +

+

+ + {brief.duration} +

+

+ + Fixed price +

+
+
+

+ {brief.description.length > 500 + ? brief.description.substring(0, 500) + : brief.description} + {brief.description.length > 500 && ( + + more + + )} +

+
+
+ {[0, 1, 2, 3].map( + (item) => + brief.skills.at(item) && ( +

+ {brief.skills.at(item)?.name} + +

+ ) + )} + {brief.skills.length - 4 > 0 && ( +

+ {brief.skills.length - 4} more +

+ )} +
+
+
+
+ profile +
+

{ + router.push(`/profile/${brief.owner_name}`); + }} + className='text-black' + > + {brief.created_by} +

+

Company Hire

+
+
+
+

+ + + + Payment method verified +

+

+ + + + {brief.number_of_briefs_submitted} +

+

+ + + + $19k total spent +

+

+ + + + 59 hires,6 active +

+
+
+ + } + /> +
+

4.68 of 40 reviews

+

Member since: Aug 17,2023

+
+
+
+
+ ); +} diff --git a/src/components/ClientView/ClientView.tsx b/src/components/ClientView/ClientView.tsx index 90b99538..eeadf523 100644 --- a/src/components/ClientView/ClientView.tsx +++ b/src/components/ClientView/ClientView.tsx @@ -1,4 +1,5 @@ -import classNames from 'classnames'; +import ArrowBackIcon from '@mui/icons-material/ChevronLeft'; +import { useRouter } from 'next/router'; import { useState } from 'react'; import { Freelancer, Project } from '@/model'; @@ -29,9 +30,41 @@ export default function ClientView({ loadingApplications, }: ClientViewProps) { const [switcher, setSwitcher] = useState('application'); + const router = useRouter(); return (
-
+
+
router.back()} + className='border border-content ml-2 group hover:bg-content rounded-full flex items-center justify-center cursor-pointer left-5 top-10' + > + +
+
+

setSwitcher('application')} + className='text-2xl text-black py-5 border-r text-center w-full ' + > + Briefs ({briefs?.briefsUnderReview?.length}) +

+

setSwitcher('projects')} + className='text-2xl text-black py-5 border-r text-center w-full' + > + Projects({briefs?.acceptedBriefs.length}) +

+

setSwitcher('grants')} + className='text-2xl text-black border-r py-5 text-center w-full' + > + Grants({ongoingGrants.length}) +

+
+
+ {/*

setSwitcher('application')} className={classNames( @@ -41,7 +74,7 @@ export default function ClientView({ : 'text-imbue-light-purple-two' )} > - Briefs ({briefs?.briefsUnderReview.length}) + Briefs ({briefs?.briefsUnderReview?.length})

setSwitcher('projects')} @@ -65,7 +98,7 @@ export default function ClientView({ > Grants({ongoingGrants.length})

-
+
*/} {/* */} {switcher === 'application' && ( diff --git a/src/components/Dashboard/FreelacerView/BriefApplication/BreifApplication.tsx b/src/components/Dashboard/FreelacerView/BriefApplication/BreifApplication.tsx index d7fcd122..715eff06 100644 --- a/src/components/Dashboard/FreelacerView/BriefApplication/BreifApplication.tsx +++ b/src/components/Dashboard/FreelacerView/BriefApplication/BreifApplication.tsx @@ -1,5 +1,6 @@ import { Divider } from '@mui/material'; import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en.json'; import router from 'next/router'; import { useState } from 'react'; @@ -8,7 +9,7 @@ import { displayState, OffchainProjectState, Project } from '@/model'; interface BreifApplicationProps { applications: any; } - +TimeAgo.addLocale(en); const timeAgo = new TimeAgo('en-US'); const BreifApplication: React.FC = ({ @@ -18,9 +19,15 @@ const BreifApplication: React.FC = ({ const loadBrefApplicationValue = 10; const [loadValue, setValue] = useState(loadBrefApplicationValue); const redirectToApplication = (application: Project) => { - router.push( - `/briefs/${application.brief_id}/applications/${application.id}/` - ); + if (application.chain_project_id) + router.push( + `/projects/${application.id}/` + ); + + else + router.push( + `/briefs/${application.brief_id}/applications/${application.id}/` + ); }; const redirectToDiscoverBriefs = () => { @@ -47,10 +54,10 @@ const BreifApplication: React.FC = ({ {applications?.map( (item: any, index: number) => index < - Math.min( - applications.length, - Math.max(loadValue, loadBrefApplicationValue) - ) && ( + Math.min( + applications.length, + Math.max(loadValue, loadBrefApplicationValue) + ) && ( <>
= ({
{displayState(item?.status_id || 0)}
diff --git a/src/components/Dashboard/MyChatBox.tsx b/src/components/Dashboard/MyChatBox.tsx index a3e6b319..452c00c6 100644 --- a/src/components/Dashboard/MyChatBox.tsx +++ b/src/components/Dashboard/MyChatBox.tsx @@ -26,12 +26,12 @@ function DashboardChatBox({ client }: { client: StreamChat }) { const filters = client && { members: { $in: [String(client.user?.id)] } }; const router = useRouter(); - useEffect(() => { - if (router.query.chat && !channel) { - router.query.chat = []; - router.replace(router, undefined, { shallow: true }); - } - }, [router.query.chat, channel, router]); + // useEffect(() => { + // if (router.query.chat && !channel) { + // router.query.chat = []; + // router.replace(router, undefined, { shallow: true }); + // } + // }, [router.query.chat, channel, router]); const closeChat = () => { //for navigating back and front @@ -57,6 +57,19 @@ function DashboardChatBox({ client }: { client: StreamChat }) { return username; }; + useEffect(() => { + if (router.query.chat) { + const channleId = router.query.chat; + const targetChannle = channels.filter( + (item: any) => item.id === channleId + ); + if (targetChannle.length) { + setChannel(targetChannle[0]); + setActiveChannel(targetChannle[0]); + } + } + }, []); + const getUserPhoto = (index: string) => { const array: any = Object.values(channels[index]?.state?.members); let profile_photo = 'Not Found'; @@ -79,7 +92,6 @@ function DashboardChatBox({ client }: { client: StreamChat }) { const handleChatClick = (selectedChannel: any) => { setChannel(selectedChannel); setActiveChannel(selectedChannel); - //for navigating back and front router.query.chat = selectedChannel.id; router.push(router, undefined, { shallow: true }); @@ -183,7 +195,7 @@ function DashboardChatBox({ client }: { client: StreamChat }) { }; return ( -
+
{mobileView ? ( <> diff --git a/src/components/Dashboard/V2/BriefsView.tsx b/src/components/Dashboard/V2/BriefsView.tsx new file mode 100644 index 00000000..d82ae5f0 --- /dev/null +++ b/src/components/Dashboard/V2/BriefsView.tsx @@ -0,0 +1,661 @@ +import { Skeleton } from '@mui/material'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import { BsFilter } from 'react-icons/bs'; + +import { strToIntRange } from '@/utils/helper'; + +import BriefComponent from '@/components/BriefComponent'; +import FilterModal from '@/components/Filter/FilterModal'; + +import { Brief, BriefSqlFilter, Item, User } from '@/model'; +import { + callSearchBriefs, + getAllBriefs, + getAllSavedBriefs, + getAllSkills, +} from '@/redux/services/briefService'; + +import { BriefFilterOption } from '@/types/briefTypes'; + +type BriefsViewProps = { + setError: (_error: any) => void; + currentUser: User; +}; + +const BriefsView = (props: BriefsViewProps) => { + const { setError, currentUser } = props; + const [briefs, setBriefs] = useState([]); + const [currentPage] = useState(1); + // FIXME: setLoading + const [loading, setLoading] = useState(true); + const [itemsPerPage, setItemsPerPage] = useState(6); + // const [pageInput, setPageInput] = useState(1); + const [searchInput, setSearchInput] = useState(''); + + const [selectedFilterIds, setSlectedFilterIds] = useState>([]); + const [savedBriefsActive, setSavedBriefsActive] = useState(false); + const [filterVisble, setFilterVisible] = useState(false); + + const router = useRouter(); + + const { + expRange, + submitRange, + lengthRange, + heading, + size: sizeProps, + skillsProps, + verified_only: verifiedOnlyProp, + } = router.query; + + useEffect(() => { + const fetchAndSetBriefs = async () => { + try { + if (!Object.keys(router?.query).length) { + const briefs_all: any = await getAllBriefs(itemsPerPage, currentPage); + if (briefs_all.status === 200) { + setBriefs(briefs_all?.currentData); + } else { + setError({ message: 'Something went wrong. Please try again' }); + } + } else { + let filter: BriefSqlFilter = { + experience_range: [], + submitted_range: [], + submitted_is_max: false, + length_range: [], + skills_range: [], + length_is_max: false, + search_input: '', + items_per_page: itemsPerPage, + page: currentPage, + verified_only: false, + non_verified: false, + }; + + const verifiedOnlyPropIndex = selectedFilterIds.indexOf('4-0'); + + // if (router.query.page) { + // const pageQuery = Number(router.query.page); + // filter.page = pageQuery; + // setCurrentPage(pageQuery); + // setPageInput(pageQuery); + // } + + if (router.query.non_verified) { + filter.non_verified = true; + } + + if (sizeProps) { + filter.items_per_page = Number(sizeProps); + setItemsPerPage(Number(sizeProps)); + } + + if (expRange) { + const range = strToIntRange(expRange); + range?.forEach?.((v: any) => { + if (!selectedFilterIds.includes(`0-${v - 1}`)) + selectedFilterIds.push(`0-${v - 1}`); + }); + + filter = { ...filter, experience_range: strToIntRange(expRange) }; + } + + if (skillsProps) { + const range = strToIntRange(skillsProps); + range?.forEach?.((v: any) => { + if (!selectedFilterIds.includes(`3-${v}`)) + selectedFilterIds.push(`3-${v}`); + }); + + filter = { ...filter, skills_range: range }; + } + + if (submitRange) { + const range = strToIntRange(submitRange); + range?.forEach?.((v: any) => { + if (v > 0 && v < 5) selectedFilterIds.push(`1-${0}`); + + if (v >= 5 && v < 10) selectedFilterIds.push(`1-${1}`); + + if (v >= 10 && v < 15) selectedFilterIds.push(`1-${2}`); + + if (v > 15) selectedFilterIds.push(`1-${3}`); + }); + filter = { ...filter, submitted_range: strToIntRange(submitRange) }; + } + if (heading) { + filter = { ...filter, search_input: heading }; + // const input = document.getElementById( + // 'search-input' + // ) as HTMLInputElement; + // if (input) input.value = heading.toString(); + setSearchInput(heading.toString()); + } + + if (verifiedOnlyProp) { + if (!selectedFilterIds.includes(`4-0`)) + selectedFilterIds.push(`4-0`); + + filter = { ...filter, verified_only: true }; + } else if (verifiedOnlyPropIndex !== -1) { + // const newFileter = [...selectedFilterIds].filter((f) => f !== '4-0') + // setSlectedFilterIds(newFileter) + selectedFilterIds.splice(verifiedOnlyPropIndex, 1); + } + + if (lengthRange) { + const range = strToIntRange(lengthRange); + range?.forEach?.((v: any) => { + if (!selectedFilterIds.includes(`2-${v - 1}`)) + selectedFilterIds.push(`2-${v - 1}`); + }); + filter = { ...filter, length_range: strToIntRange(lengthRange) }; + } + + let result: any = []; + + if (savedBriefsActive) { + result = await getAllSavedBriefs( + filter.items_per_page || itemsPerPage, + currentPage, + currentUser?.id + ); + } else { + result = await callSearchBriefs(filter); + } + + if (result.status === 200 || result.totalBriefs !== undefined) { + const totalPages = Math.ceil( + result?.totalBriefs / (filter?.items_per_page || 6) + ); + + if (totalPages < filter.page && totalPages > 0) { + router.query.page = totalPages.toString(); + router.push(router, undefined, { shallow: true }); + filter.page = totalPages; + } + + setBriefs(result?.currentData); + } else { + setError({ message: 'Something went wrong. Please try again' }); + } + } + } catch (error) { + setError({ message: 'Something went wrong. Please try again' + error }); + } finally { + setLoading(false); + } + }; + + router.isReady && fetchAndSetBriefs(); + }, [ + router.isReady, + currentPage, + itemsPerPage, + router, + setError, + selectedFilterIds, + sizeProps, + expRange, + skillsProps, + submitRange, + heading, + verifiedOnlyProp, + lengthRange, + savedBriefsActive, + currentUser?.id, + ]); + + const expfilter = { + // This is a table named "experience" + // If you change this you must remigrate the experience table and add the new field. + filterType: BriefFilterOption.ExpLevel, + label: 'Experience Level', + options: [ + { + interiorIndex: 0, + search_for: [1], + value: 'Entry Level', + or_max: false, + }, + { + interiorIndex: 1, + search_for: [2], + value: 'Intermediate', + or_max: false, + }, + { + interiorIndex: 2, + search_for: [3], + value: 'Expert', + or_max: false, + }, + { + interiorIndex: 3, + search_for: [4], + value: 'Specialist', + or_max: false, + }, + ], + }; + + const submittedFilters = { + // This is a field associated with the User. + // since its a range i need the + filterType: BriefFilterOption.AmountSubmitted, + label: 'Briefs Submitted', + options: [ + { + interiorIndex: 0, + search_for: [1, 2, 3, 4], + value: '1-4', + or_max: false, + }, + { + interiorIndex: 1, + search_for: [5, 6, 7, 8, 9], + value: '5-9', + or_max: false, + }, + { + interiorIndex: 2, + search_for: [10, 11, 12, 13, 14], + value: '10-14', + or_max: false, + }, + { + interiorIndex: 3, + search_for: [15, 10000], + value: '15+', + or_max: true, + }, + ], + }; + + const lengthFilters = { + // Should be a field in the database, WILL BE IN DAYS. + + // Again i need the high and low values. + filterType: BriefFilterOption.Length, + label: 'Project Length', + options: [ + { + interiorIndex: 0, + search_for: [1], + value: '1-3 months', + or_max: false, + }, + { + interiorIndex: 1, + search_for: [2], + value: '3-6 months', + or_max: false, + }, + { + interiorIndex: 2, + search_for: [3], + value: '6-12 months', + or_max: false, + }, + { + interiorIndex: 3, + search_for: [12], + or_max: true, + value: '1 year +', + }, + { + // years * months + interiorIndex: 4, + search_for: [12 * 5], + or_max: true, + value: '5 years +', + }, + ], + }; + + // const hoursPwFilter = { + // filterType: BriefFilterOption.HoursPerWeek, + // label: 'Hours Per Week', + // options: [ + // { + // interiorIndex: 0, + // // This will be 0-30 as we actually use this as max value + // search_for: [30], + // or_max: false, + // value: '30hrs/week', + // }, + // { + // interiorIndex: 1, + // // Same goes for this + // search_for: [50], + // value: '50hrs/week', + // or_max: true, + // }, + // ], + // }; + + const [skills, setSkills] = useState([{ name: '', id: 0 }]); + + useEffect(() => { + const getAllFilters = async () => { + const filteredItems = await getAllSkills(); + setSkills(filteredItems?.skills); + }; + + getAllFilters(); + }, []); + + const skillsFilter = { + filterType: BriefFilterOption.Skills, + label: 'Skills required', + options: skills?.map((s) => ({ + interiorIndex: s.id, + search_for: [s.id], + value: s.name, + })), + }; + + const freelancerInfoFilter = { + filterType: BriefFilterOption.FreelancerInfo, + label: 'Freelancer Info', + options: [ + { + interiorIndex: 0, + value: 'Verified', + }, + { + interiorIndex: 1, + value: 'Non-verified', + }, + ], + }; + + const customDropdownConfigs = [ + { + label: 'Project Length', + filterType: BriefFilterOption.Length, + options: lengthFilters.options, + }, + { + label: 'Proposal Submitted', + filterType: BriefFilterOption.AmountSubmitted, + options: submittedFilters.options, + }, + { + label: 'Experience Level', + filterType: BriefFilterOption.ExpLevel, + options: expfilter.options, + }, + { + label: 'Skills Required', + filterType: BriefFilterOption.Skills, + options: skillsFilter.options, + }, + { + label: 'Freelancer Information', + filterType: BriefFilterOption.FreelancerInfo, + options: freelancerInfoFilter.options, + }, + // { + // name: 'Hours Per Week', + // filterType: BriefFilterOption.HoursPerWeek, + // filterOptions: hoursPwFilter.options, + // }, + ]; + + // Here we have to get all the checked boxes and try and construct a query out of it... + const onSearch = async () => { + // The filter initially should return all values + setLoading(true); + let is_search = false; + + let exp_range: number[] = []; + let submitted_range: number[] = []; + let submitted_is_max = false; + let length_range: number[] = []; + let length_is_max = false; + let length_range_prop: number[] = []; + let skills_prop: number[] = []; + let verified_only = false; + let non_verified = false; + + // default is max + // const hpw_max = 50; + // const hpw_is_max = false; + const search_value = searchInput; + if (search_value !== '') { + is_search = true; + } + + for (let i = 0; i < selectedFilterIds.length; i++) { + if (selectedFilterIds[i] !== '') { + is_search = true; + const id = selectedFilterIds[i]; + if (id != null) { + const [filterType, interiorIndex] = id.split('-'); + // Here we are trying to build teh paramaters required to build the query + // We build an array for each to get the values we want through concat. + // and also specify if we want more than using the is_max field. + + switch (parseInt(filterType) as BriefFilterOption) { + case BriefFilterOption.ExpLevel: + { + const o = expfilter.options[parseInt(interiorIndex)]; + exp_range = [...exp_range, ...o.search_for.slice()]; + } + break; + + case BriefFilterOption.AmountSubmitted: + { + const o1 = submittedFilters.options[parseInt(interiorIndex)]; + submitted_range = [ + ...submitted_range, + ...o1.search_for.slice(), + ]; + submitted_is_max = o1.or_max; + } + break; + + case BriefFilterOption.Length: + { + const o2 = lengthFilters.options[parseInt(interiorIndex)]; + length_range = [...length_range, ...o2.search_for.slice()]; + length_is_max = o2.or_max; + + if (o2.search_for[0] === 12) + length_range_prop = [...length_range_prop, 4]; + else if (o2.search_for[0] === 60) + length_range_prop = [...length_range_prop, 5]; + else length_range_prop = length_range; + } + break; + + case BriefFilterOption.Skills: + { + skills_prop = [...skills_prop, parseInt(interiorIndex)]; + } + break; + + case BriefFilterOption.FreelancerInfo: + { + if (parseInt(interiorIndex) === 0) verified_only = true; + if (parseInt(interiorIndex) === 1) non_verified = true; + } + break; + + default: + // eslint-disable-next-line no-console + console.log( + 'Invalid filter option selected or unimplemented. type:' + + filterType + ); + } + } + } + } + + router.query.page = '1'; + router.query.verified_only = verified_only ? '1' : []; + router.query.non_verified = non_verified ? '1' : []; + router.query.heading = search_value !== '' ? search_value : []; + router.query.expRange = exp_range.length ? exp_range.toString() : []; + router.query.submitRange = submitted_range.length + ? submitted_range.toString() + : []; + router.query.submitted_is_max = submitted_is_max + ? submitted_is_max.toString() + : []; + router.query.lengthRange = length_range_prop.length + ? length_range_prop.toString() + : []; + router.query.skillsProps = skills_prop.length ? skills_prop.toString() : []; + router.push(router, undefined, { shallow: true }); + + try { + if (is_search) { + const filter: BriefSqlFilter = { + experience_range: exp_range, + submitted_range, + submitted_is_max, + length_range, + length_is_max, + skills_range: skills_prop, + search_input: search_value, + items_per_page: itemsPerPage, + page: 1, + verified_only: verified_only, + non_verified: non_verified, + }; + + if (search_value.length === 0) { + setFilterVisible(!filterVisble); + } + + const briefs_filtered: any = await callSearchBriefs(filter); + + setBriefs(briefs_filtered?.currentData); + } else { + const briefs_all: any = await getAllBriefs(itemsPerPage, currentPage); + + setBriefs(briefs_all?.currentData); + } + } catch (error) { + setError({ message: error }); + } finally { + setLoading(false); + setFilterVisible(false); + } + }; + + const toggleFilter = () => { + setFilterVisible(!filterVisble); + }; + + const reset = async () => { + setSavedBriefsActive(false); + await router.push({ + pathname: router.pathname, + query: {}, + }); + const allBriefs: any = await getAllBriefs(itemsPerPage, currentPage); + setSlectedFilterIds([]); + setBriefs(allBriefs?.currentData); + setSearchInput(''); + }; + + const cancelFilters = async () => { + reset(); + setFilterVisible(false); + }; + + const handleSetId = (id: string | string[]) => { + if (Array.isArray(id)) { + setSlectedFilterIds([...id]); + } else { + setSlectedFilterIds([...selectedFilterIds, id]); + } + }; + + return ( +
+
+

Recomended Briefs

+
+
router.push('/briefs')} + > + +

view all

+
+
+
+ {loading ? ( +
+ {[1, 2, 3].map((item) => ( +
+
+ +
+ + + +
+ + + + + +
+ + + +
+
+ +
+
+ +
+ + +
+
+ + + + + +
+
+ ))} +
+ ) : ( + <> + {briefs.map((brief) => ( + + ))} + + )} + + toggleFilter()} + {...{ + cancelFilters, + handleSetId, + onSearch, + customDropdownConfigs, + selectedFilterIds, + }} + /> +
+ ); +}; + +export default BriefsView; diff --git a/src/components/FreelancerCard/FreelancerCard.tsx b/src/components/FreelancerCard/FreelancerCard.tsx new file mode 100644 index 00000000..87f761f3 --- /dev/null +++ b/src/components/FreelancerCard/FreelancerCard.tsx @@ -0,0 +1,96 @@ +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { AiOutlineHeart } from 'react-icons/ai'; +import { BiMessageRoundedDetail } from 'react-icons/bi'; +import { HiOutlineTicket } from 'react-icons/hi'; + +import { Freelancer } from '@/model'; + +export default function FreelancerCard({ + freelancer, + handleMessage, +}: { + freelancer: Freelancer; + handleMessage: any; +}) { + const router = useRouter(); + return ( +
+
+ freelancer profile +
router.push(`/freelancers/${freelancer?.username}`)} + > +

+ {freelancer?.display_name?.substring(0, 15)}. +

+

+ {freelancer?.title?.substring(0, 20)} + {freelancer?.title?.length > 20 && '...'} +

+
+

+ Available +

+

+
+ {/*
+

+ $50-$75 hr +

+

+ Job Success rate 99.2% +

+
*/} +
+ {[1, 2, 3, 4].map( + (item: number, index: number) => + index < freelancer?.skills?.length && ( +

+ + {freelancer?.skills?.at(index)?.name} +

+ ) + )} + {freelancer?.skills?.length > 4 && ( +

+ + {freelancer?.skills?.length - 4} +

+ )} +
+
+
+

+ +

+

handleMessage(freelancer?.user_id)} + className='bg-imbue-purple flex items-center gap-1 justify-center px-7 py-2 w-full text-white text-sm rounded-full cursor-pointer' + > + Connect Freelancer + +

+
+
+ ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 86cd356b..ede3b452 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -30,15 +30,29 @@ export interface LoginPopupStateType { redirectURL?: string; } +type ProfileMode = 'client' | 'freelancer'; + export interface LoginPopupContextType { showLoginPopUp?: LoginPopupStateType; setShowLoginPopup: (_value: LoginPopupStateType) => void; } +export interface AppContextType { + profileView?: ProfileMode; + // setProfileView: (_value: ProfileMode) => void; + setProfileMode: (_mode: ProfileMode) => void; +} + export const LoginPopupContext = createContext( null ); +// TODO: Include screens to this context +export const AppContext = createContext( + null +); + + function Layout({ children }: LayoutProps) { const [progress, setProgress] = useState(0); const [showLoginPopUp, setShowLoginPopup] = useState({ @@ -57,6 +71,7 @@ function Layout({ children }: LayoutProps) { router.events.off('routeChangeError', () => setProgress(100)); }; }, [router]); + const theme = createTheme({ palette: { primary: { @@ -67,6 +82,21 @@ function Layout({ children }: LayoutProps) { }, }, }); + + // Profile switching + const [profileView, setProfileView] = useState('client'); + + useEffect(() => { + const profileView = localStorage.getItem('profileView') as ProfileMode; + + if (profileView) setProfileView(profileView); + }, []); + + const setProfileMode = (mode: ProfileMode) => { + localStorage.setItem('profileView', mode); + setProfileView(mode); + }; + return ( @@ -80,27 +110,35 @@ function Layout({ children }: LayoutProps) { /> )} - {!( - router.asPath === '/join' || - router.asPath === '/auth/sign-up' || - router.asPath === '/auth/sign-in' - ) && } - -
- } + +
- {children} - -
- + + {children} + +
+ +
diff --git a/src/components/MessageComponent/index.tsx b/src/components/MessageComponent/index.tsx new file mode 100644 index 00000000..93297f41 --- /dev/null +++ b/src/components/MessageComponent/index.tsx @@ -0,0 +1,51 @@ +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en.json'; +import Image from 'next/image'; +import { DefaultGenerics, FormatMessageResponse } from 'stream-chat'; + +TimeAgo.addLocale(en); + +const timeAgo = new TimeAgo('en-US'); + +export default function MessageComponent({ + handleMessageClick, + props, +}: { + props: FormatMessageResponse; + handleMessageClick: any; +}) { + const handleClick = () => { + // router.push({ + // pathname: `/dashboard/messages`, + // query: `chat=${props.cid?.split(':')[1]}`, + // }); + handleMessageClick(props?.user?.id); + }; + + return ( +
+ profile image +
+
+

{props?.user?.name}

+

{props?.text}

+
+

+ {props?.created_at && timeAgo.format(new Date(props?.created_at))} +

+
+
+ ); +} diff --git a/src/components/Navbar/NewNavBar.tsx b/src/components/Navbar/NewNavBar.tsx index c184732b..34d8a4eb 100644 --- a/src/components/Navbar/NewNavBar.tsx +++ b/src/components/Navbar/NewNavBar.tsx @@ -16,7 +16,7 @@ import { import dynamic from 'next/dynamic'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { BiBuildings } from 'react-icons/bi'; import { BsPeople } from 'react-icons/bs'; import { IoIosArrowDown } from 'react-icons/io'; @@ -35,6 +35,7 @@ const Login = dynamic(() => import('../Login')); import Link from 'next/link'; import NotificationIcon from './NotificationIcon'; +import { AppContext, AppContextType } from '../Layout'; import LoginPopup from '../LoginPopup/LoginPopup'; import defaultProfile from '../../assets/images/profile-image.png'; @@ -99,6 +100,8 @@ function NewNavbar() { const navPillclasses = 'text-imbue-purple-dark h-[3rem] bg-white rounded-[5.07319rem] !flex justify-center items-center px-5 hover:no-underline !text-[1rem] '; + const { profileView, setProfileMode } = useContext(AppContext) as AppContextType + return ( <>
@@ -185,59 +188,59 @@ function NewNavbar() {
- {user?.id - ? ( -
-
- page icon - Submit - -
+ {user?.id ? ( +
+
+ page icon + Submit + +
-
-
-
{ - router.push('/grants/new'); - }} - className='flex gap-2 px-2 hover:bg-imbue-lime-light items-center py-2 rounded-md ' - > -
- -
-
-

Submit Grant

-
+
+
+
{ + router.push('/grants/new'); + }} + className='flex gap-2 px-2 hover:bg-imbue-lime-light items-center py-2 rounded-md ' + > +
+ +
+
+

Submit Grant

+
+
+
{ + router.push('/briefs/new'); + }} + className='flex gap-2 px-2 items-center hover:bg-imbue-lime-light py-2 rounded-md ' + > +
+
-
{ - router.push('/briefs/new'); - }} - className='flex gap-2 px-2 items-center hover:bg-imbue-lime-light py-2 rounded-md ' - > -
- -
-
-

Submit Brief

-
+
+

Submit Brief

-
) - : "" - } +
+
+ ) : ( + '' + )} setExpanded(false)} className={`mx-1 hover:bg-imbue-lime-light text-xs lg:text-sm hidden lg:inline-block cursor-pointer ${navPillclasses} nav-item nav-item-2`} @@ -252,10 +255,9 @@ function NewNavbar() { /> Wallet - {/* setExpanded(false)} className={`mx-1 relative group text-xs hover:bg-imbue-lime-light lg:text-sm hidden lg:inline-block cursor-pointer hover:underline ${navPillclasses}`} - href='#' > -
-
-
- -
-
-

Switch to Hiring

-
+
+
+ {profileView === 'client' ? ( +
{ + setProfileMode('freelancer') + router.push('/dashboard') + }} + > +
+ +
+
+

Switch to Freelancer

+
+
+ ) : ( +
{ + setProfileMode('client') + router.push('/dashboard') + }} + > +
+ +
+
+

Switch to Hiring

+
+
+ )}
- */} +
diff --git a/src/components/PopupScreens/SwitchToFreelancer.tsx b/src/components/PopupScreens/SwitchToFreelancer.tsx new file mode 100644 index 00000000..ed27c348 --- /dev/null +++ b/src/components/PopupScreens/SwitchToFreelancer.tsx @@ -0,0 +1,173 @@ +import { Dialog } from '@mui/material'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import React, { useContext, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { getFreelancerProfile } from '@/redux/services/freelancerService'; +import { RootState } from '@/redux/store/store'; + +import FullScreenLoader from '../FullScreenLoader'; +import { AppContext, AppContextType } from '../Layout'; + + +const SwitchToFreelancer = () => { + const router = useRouter() + + const { setProfileMode } = useContext(AppContext) as AppContextType + + const { user, loading: loadingUser } = useSelector( + (state: RootState) => state.userState + ); + + const [open, setOpen] = useState(true) + const [success, setSuccess] = useState(false) + const [loading, setloading] = useState(true) + const [isFreelancer, setIsFreelancer] = useState(false) + + useEffect(() => { + setloading(true) + + const checkFreelancerProfile = async () => { + try { + const freelancer = await getFreelancerProfile(user?.username); + if (!freelancer?.id) setIsFreelancer(false); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + setloading(false); + } + } + checkFreelancerProfile() + }, [loadingUser, router, user?.username]); + + if (loadingUser || loading) return + + return ( + setOpen(false)} + aria-labelledby='alert-dialog-title' + aria-describedby='alert-dialog-description' + className='p-14 errorDialogue' + > + { + !isFreelancer + ? ( +
+
+ + + + +
+
+
+ +
+

+ You do not have a freelancer account. +

+

+ Do you want to join as a freelancer to complete this action? +

+
+ +
+ + +
+
+ ) + : ( +
+ { + success + ? ( +
+
+
+ + +
+
+
+
+ +
+

+ Congratulations! +

+

You are now in freelancer profile

+
+ +
+ + {/* */} +
+
+ ) + : ( +
+
+ +
+ +
+

You are currently in client profile.

+

You must switch to freelancer profile for accessing this page

+
+
+ + +
+
+ ) + } +
+ ) + } + +
+ ); +}; + +export default SwitchToFreelancer; diff --git a/src/lib/models.ts b/src/lib/models.ts index 69c6b1b6..14dee155 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -740,6 +740,8 @@ export const fetchAllBriefs = () => (tx: Knex.Transaction) => 'briefs.duration_id', 'budget', 'users.display_name as created_by', + 'users.profile_photo as owner_photo', + 'users.username as owner_name', 'experience_level', 'briefs.experience_id', 'briefs.created', @@ -768,7 +770,8 @@ export const fetchAllBriefs = () => (tx: Knex.Transaction) => .groupBy('users.display_name') .groupBy('briefs.experience_id') .groupBy('experience.experience_level') - .groupBy('users.id'); + .groupBy('users.id') + .groupBy('users.username'); export const fetchAllGrants = () => (tx: Knex.Transaction) => tx diff --git a/src/model.ts b/src/model.ts index e634421f..86ff7d6a 100644 --- a/src/model.ts +++ b/src/model.ts @@ -74,10 +74,10 @@ export enum ImbueChainPollResult { } export type VotesResp = { - yes: User[]; - no: User[]; - pending: User[]; -} + yes: User[]; + no: User[]; + pending: User[]; +}; export type Project = { id?: string | number; @@ -275,6 +275,8 @@ export type Brief = { project_id?: number; currentUserId?: number; verified_only: boolean; + owner_name: string; + owner_photo: string; }; export type BriefSqlFilter = { diff --git a/src/pages/api/auth/common.ts b/src/pages/api/auth/common.ts index d637f827..c0ef4e7b 100644 --- a/src/pages/api/auth/common.ts +++ b/src/pages/api/auth/common.ts @@ -37,7 +37,6 @@ export const authenticate = ( )(req, res); }); - export function verifyUserIdFromJwt(req: any, res: any, user_ids: number[]) { const token = getTokenCookie(req); if (!token) { diff --git a/src/pages/briefs/[id].tsx b/src/pages/briefs/[id].tsx index b3b1c70f..b778ff3e 100644 --- a/src/pages/briefs/[id].tsx +++ b/src/pages/briefs/[id].tsx @@ -53,7 +53,9 @@ const BriefDetails = (): JSX.Element => { experience_id: 0, number_of_briefs_submitted: 0, user_id: 0, - verified_only: false + verified_only: false, + owner_name: '', + owner_photo: '', }); // const [browsingUser, setBrowsingUser] = useState(null); @@ -65,7 +67,9 @@ const BriefDetails = (): JSX.Element => { const [targetUser, setTargetUser] = useState(null); const [showMessageBox, setShowMessageBox] = useState(false); // const [showLoginPopup, setShowLoginPopup] = useState(false); - const { setShowLoginPopup } = useContext(LoginPopupContext) as LoginPopupContextType + const { setShowLoginPopup } = useContext( + LoginPopupContext + ) as LoginPopupContextType; const isOwnerOfBrief = browsingUser && browsingUser.id == brief.user_id; const [error, setError] = useState(); @@ -107,10 +111,12 @@ const BriefDetails = (): JSX.Element => { }, [id, browsingUser.username]); const redirectToApply = () => { - if (browsingUser?.id) - router.push(`/briefs/${brief.id}/apply`); + if (browsingUser?.id) router.push(`/briefs/${brief.id}/apply`); else - setShowLoginPopup({ open: true, redirectURL: `/briefs/${brief.id}/apply` }); + setShowLoginPopup({ + open: true, + redirectURL: `/briefs/${brief.id}/apply`, + }); }; const handleMessageBoxClick = async () => { @@ -123,8 +129,11 @@ const BriefDetails = (): JSX.Element => { }; const saveBrief = async () => { - - if (!browsingUser?.id) return setShowLoginPopup({ open: true, redirectURL: `/briefs/${brief.id}`}); + if (!browsingUser?.id) + return setShowLoginPopup({ + open: true, + redirectURL: `/briefs/${brief.id}`, + }); const resp = await saveBriefData({ ...brief, @@ -160,14 +169,16 @@ const BriefDetails = (): JSX.Element => { return (

Similar projects on Imbue

setShowSimilarBrief(!showSimilarBrief)} diff --git a/src/pages/briefs/[id]/apply.tsx b/src/pages/briefs/[id]/apply.tsx index 06ed33b8..07a69310 100644 --- a/src/pages/briefs/[id]/apply.tsx +++ b/src/pages/briefs/[id]/apply.tsx @@ -5,7 +5,7 @@ import { Tooltip } from '@mui/material'; import { WalletAccount } from '@talismn/connect-wallets'; import Filter from 'bad-words'; import { useRouter } from 'next/router'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { FiPlusCircle } from 'react-icons/fi'; import { useSelector } from 'react-redux'; @@ -19,6 +19,8 @@ import AccountChoice from '@/components/AccountChoice'; import { BriefInsights } from '@/components/Briefs/BriefInsights'; import ErrorScreen from '@/components/ErrorScreen'; import FullScreenLoader from '@/components/FullScreenLoader'; +import { AppContext, AppContextType } from '@/components/Layout'; +import SwitchToFreelancer from '@/components/PopupScreens/SwitchToFreelancer'; import SuccessScreen from '@/components/SuccessScreen'; import * as config from '@/config'; @@ -68,6 +70,7 @@ export const SubmitProposal = (): JSX.Element => { const router = useRouter(); const briefId: any = router?.query?.id || 0; + const { profileView } = useContext(AppContext) as AppContextType const [applicationId, setapplicationId] = useState(); const [error, setError] = useState(); @@ -81,24 +84,24 @@ export const SubmitProposal = (): JSX.Element => { }, [applicationId]); useEffect(() => { + const getUserAndFreelancer = async () => { + const freelancer = await getFreelancerProfile(user?.username); + // if (!freelancer?.id) router.push(`/freelancers/new`); + setFreelancer(freelancer); + + const userApplication: any = await getFreelancerBrief(user?.id, briefId); + if (userApplication && profileView === 'freelancer') { + router.push(`/briefs/${briefId}/applications/${userApplication?.id}/`); + } + }; + !loadingUser && getUserAndFreelancer(); - }, [briefId, user?.username, loadingUser]); + }, [briefId, user?.username, loadingUser, profileView]); useEffect(() => { router?.isReady && getCurrentUserBrief(); }, [user, router.isReady]); - const getUserAndFreelancer = async () => { - const freelancer = await getFreelancerProfile(user?.username); - if (!freelancer?.id) router.push(`/freelancers/new`); - setFreelancer(freelancer); - - const userApplication: any = await getFreelancerBrief(user?.id, briefId); - if (userApplication) { - router.push(`/briefs/${briefId}/applications/${userApplication?.id}/`); - } - }; - const getCurrentUserBrief = async () => { if (briefId && user) { setLoading(true); @@ -274,6 +277,7 @@ export const SubmitProposal = (): JSX.Element => { // const milestoneAmountsAndNamesHaveValue = allAmountAndNamesHaveValue(); + if (loadingUser || loading) ; return ( @@ -554,9 +558,8 @@ export const SubmitProposal = (): JSX.Element => { title={disableSubmit && 'Please fill all the required input fields'} >
+ + { + profileView === 'client' && ( + + ) + } +
); }; diff --git a/src/pages/briefs/index.tsx b/src/pages/briefs/index.tsx index 964cac76..8dd363d8 100644 --- a/src/pages/briefs/index.tsx +++ b/src/pages/briefs/index.tsx @@ -2,13 +2,13 @@ /* eslint-disable react-hooks/exhaustive-deps */ import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded'; import { InputAdornment, OutlinedInput, TextField } from '@mui/material'; -const IconButton = dynamic(() => import("@mui/material/IconButton"), { +const IconButton = dynamic(() => import('@mui/material/IconButton'), { ssr: false, -}) +}); // import ClearIcon from '@mui/icons-material/Clear'; -const ClearIcon = dynamic(() => import("@mui/icons-material/Clear"), { +const ClearIcon = dynamic(() => import('@mui/icons-material/Clear'), { ssr: false, -}) +}); import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en'; @@ -77,6 +77,7 @@ const Briefs = (): JSX.Element => { const [skills, setSkills] = useState([{ name: '', id: 0 }]); const [myApplications, _setMyApplications] = useState(); const [error, setError] = useState(); + const { expRange, submitRange, @@ -348,7 +349,7 @@ const Briefs = (): JSX.Element => { } if (router.query.non_verified) { - filter.non_verified = true + filter.non_verified = true; } if (sizeProps) { @@ -748,21 +749,26 @@ const Briefs = (): JSX.Element => { placeholder='Search' value={searchInput} onChange={(e) => setSearchInput(e.target.value)} - onKeyUp={e => e.key === 'Enter' && !savedBriefsActive && onSearch()} + onKeyUp={(e) => + e.key === 'Enter' && !savedBriefsActive && onSearch() + } endAdornment={ - searchInput?.length - ? ( - - searchInput?.length && setSearchInput("")} - onMouseDown={e => e.preventDefault()} - edge="end" - > - - - ) - : "" + searchInput?.length ? ( + + + searchInput?.length && setSearchInput('') + } + onMouseDown={(e) => e.preventDefault()} + edge='end' + > + + + + ) : ( + '' + ) } /> diff --git a/src/pages/briefs/mybriefs.tsx b/src/pages/briefs/mybriefs.tsx new file mode 100644 index 00000000..8246b2bb --- /dev/null +++ b/src/pages/briefs/mybriefs.tsx @@ -0,0 +1,53 @@ +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { fetchUser } from '@/utils'; + +import ChatPopup from '@/components/ChatPopup'; +import MyClientBriefsView from '@/components/Dashboard/MyClientBriefsView'; + +import { User } from '@/model'; +import { RootState } from '@/redux/store/store'; + +export default function MyBriefs() { + const { user } = useSelector((state: RootState) => state.userState); + const [showMessageBox, setShowMessageBox] = useState(false); + const [targetUser, setTargetUser] = useState(null); + const router = useRouter(); + const { briefId } = router.query; + const handleMessageBoxClick = async (user_id: number) => { + if (user_id) { + setShowMessageBox(true); + setTargetUser(await fetchUser(user_id)); + } else { + //TODO: check if user is logged in + // redirect("login", `/dapp/freelancers/${freelancer?.username}/`); + } + }; + + const redirectToBriefApplications = (applicationId: string) => { + router.push(`/briefs/${briefId}/applications/${applicationId}`); + }; + return ( +
+ + {user && showMessageBox && ( + + )} +
+ ); +} diff --git a/src/pages/dashboard/ClientDashboard.tsx b/src/pages/dashboard/ClientDashboard.tsx new file mode 100644 index 00000000..9b5e8a4b --- /dev/null +++ b/src/pages/dashboard/ClientDashboard.tsx @@ -0,0 +1,331 @@ +import classNames from 'classnames'; +import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from 'react'; +import { BiChevronDown } from 'react-icons/bi'; +import { BsFilter } from 'react-icons/bs'; +import { IoIosArrowBack } from 'react-icons/io'; +import { MdOutlineAttachMoney } from 'react-icons/md'; +import { VscNewFile } from 'react-icons/vsc'; +import { useSelector } from 'react-redux'; +// Import Swiper React components +import { Swiper, SwiperSlide, useSwiper } from 'swiper/react'; +// Import Swiper styles +import 'swiper/css'; + +import { fetchUser } from '@/utils'; + +import ChatPopup from '@/components/ChatPopup'; +import FreelancerCard from '@/components/FreelancerCard/FreelancerCard'; +import FullScreenLoader from '@/components/FullScreenLoader'; + +import { Freelancer, User } from '@/model'; +import { getUserBriefs } from '@/redux/services/briefService'; +import { getAllFreelancers } from '@/redux/services/freelancerService'; +import { getUsersOngoingGrants } from '@/redux/services/projectServices'; +import { RootState } from '@/redux/store/store'; +export function Controller() { + const sp = useSwiper(); + const [click, setClick] = useState(false); + const handleForward = () => { + sp.slideNext(); + setClick(!click); + }; + const handleBackward = () => { + sp.slidePrev(); + setClick(!click); + }; + + return ( +
+

Recommended for you ✨

+
+

+ Discover More{' '} + + +

+
+ ); +} + +const options = [ + { name: 'Accepted', bg: 'bg-[#90DB00]', status_id: 4 }, + { name: 'Pending', bg: 'bg-[#FF7A00]', status_id: 1 }, + { name: 'Completed', bg: 'bg-[#3B27C1]', status_id: 6 }, + { name: 'Refunded', bg: 'bg-[#FF7A00]', status_id: 5 }, +]; + +export default function ClientDashboard() { + const [openedOption, setOpenedOption] = useState(false); + const { user, loading: loadingUser } = useSelector( + (state: RootState) => state.userState + ); + const [showMessageBox, setShowMessageBox] = useState(false); + const [targetUser, setTargetUser] = useState(null); + const [recomdedFreelancer, setRecomendedFreelancer] = useState( + [] + ); + const [Briefs, _setBriefs] = useState(); + const [Grants, setOngoingGrants] = useState(); + const [filteredGrants, setFilteredGrants] = useState([]); + const [filterGrantoptions, setFilterGrantoptions] = useState(options[0]); + + const router = useRouter(); + + const handleMessageBoxClick = async (user_id: number) => { + if (user_id) { + setShowMessageBox(true); + setTargetUser(await fetchUser(user_id)); + } else { + //TODO: check if user is logged in + // redirect("login", `/dapp/freelancers/${freelancer?.username}/`); + } + }; + + useEffect(() => { + try { + const getFreelancers = async () => { + const data = await getAllFreelancers(12, 1); + setRecomendedFreelancer(data?.currentData); + }; + getFreelancers(); + } catch (err: any) { + //eslint-disable-next-line no-console + console.log(err); + } + }, []); + + useEffect(() => { + if (Grants?.length && Grants?.length > 0) { + const filter: any[] = Grants.filter( + (grant) => grant.status_id === filterGrantoptions.status_id + ); + setFilteredGrants(filter); + } + }, [Grants, filterGrantoptions]); + + useEffect(() => { + const setUserBriefs = async () => { + if (user?.id) _setBriefs(await getUserBriefs(user?.id)); + setOngoingGrants( + await getUsersOngoingGrants(user?.web3_address as string) + ); + }; + setUserBriefs(); + }, [user, user?.id, user?.web3_address]); + + const totalSpent = useMemo(() => { + const total = Briefs?.acceptedBriefs?.reduce( + (acc: number, item: any) => + acc + Number(item.project.total_cost_without_fee), + 0 + ); + return total; + }, [Briefs]); + + const handleGrantRedirect = () => { + router.push({ + pathname: '/grants/ongoinggrants', + query: `statusId=${filterGrantoptions.status_id}`, + }); + }; + + if (loadingUser) return ; + return ( +
+
+
+

+ Welcome, {user?.display_name?.split(' ')[0]} 👋 +

+

+ Glad to have you on imbue +

+
+ +
+ {/* starting of the box sections */} +
+
+
+

Projects

+ +

router.push('/briefs/mybriefs')} + className='bg-imbue-purple px-7 py-2 text-white text-sm rounded-full cursor-pointer' + > + View all +

+
+
+
+
+

Briefs

+
+

+ {Briefs?.briefsUnderReview?.length || 0} +

+
+
+
+
+
+

Projects

+
+

+ {Briefs?.acceptedBriefs?.length || 0} +

+
+
+
+
+
+

Grants

+
+

+ {Grants?.length || 0} +

+
+
+
+
+
+

Grants

+
+
setOpenedOption((prev) => !prev)} + > +
{' '} +

{filterGrantoptions.name}

+ +
+ +
+ {options.map((option, index) => ( +
{ + setFilterGrantoptions(option); + setOpenedOption(false); + }} + > +
+

{option.name}

+
+ ))} +
+
+
+
+
+
+

+ {filteredGrants.length} +

+

{filterGrantoptions.name} Grants

+
+

+ View all +

+
+
+
+
+
+

Total Spent

+
+
+
+
+ +

{totalSpent}

+
+

Payout Accounts

+
+

router.push('/relay')} + className='bg-imbue-purple px-7 py-2 text-white text-sm rounded-full cursor-pointer' + > + Get Started +

+
+
+
+ {/* ending of the box sections */} +
+ {/* Freelancer recomendations */} + +
+ +
+ + {recomdedFreelancer?.map((item) => ( + + + + ))} +
+
+
router.push('/freelancers')} + > + +

view all

+
+
+
+ {user && showMessageBox && ( + + )} +
+ ); +} diff --git a/src/pages/dashboard/[params].tsx b/src/pages/dashboard/[params].tsx deleted file mode 100644 index 9b19b451..00000000 --- a/src/pages/dashboard/[params].tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Dashboard from './index'; - -export default function DynamicDashboard(props: any) { - return ; -} - -export async function getServerSideProps(context: any) { - return { - props: context.params, // will be passed to the page component as props - }; -} diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 91877867..24ce50f6 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -1,33 +1,31 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ -import BottomNavigation from '@mui/material/BottomNavigation'; -import BottomNavigationAction from '@mui/material/BottomNavigationAction'; -import { StyledEngineProvider } from '@mui/material/styles'; + import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { StreamChat } from 'stream-chat'; import 'stream-chat-react/dist/css/v2/index.css'; -import { fetchUser, getStreamChat } from '@/utils'; +import { getStreamChat } from '@/utils'; -import ChatPopup from '@/components/ChatPopup'; -import DashboardChatBox from '@/components/Dashboard/MyChatBox'; -import MyClientBriefsView from '@/components/Dashboard/MyClientBriefsView'; -import MyFreelancerApplications from '@/components/Dashboard/MyFreelancerApplications'; import ErrorScreen from '@/components/ErrorScreen'; import FullScreenLoader from '@/components/FullScreenLoader'; const LoginPopup = dynamic(() => import('@/components/LoginPopup/LoginPopup')); // import LoginPopup from '@/components/LoginPopup/LoginPopup'; -import { Freelancer, Project, User } from '@/model'; +import { AppContext, AppContextType } from '@/components/Layout'; + +import { Project, User } from '@/model'; import { Brief } from '@/model'; -import { getFreelancerApplications } from '@/redux/services/freelancerService'; import { setUnreadMessage } from '@/redux/slices/userSlice'; import { RootState } from '@/redux/store/store'; +import ClientDashboard from './ClientDashboard'; +import FreelancerDashboard from './new'; + export type DashboardProps = { user: User; isAuthenticated: boolean; @@ -35,7 +33,7 @@ export type DashboardProps = { myApplicationsResponse: Project[]; }; -const Dashboard = ({ val }: { val?: string }): JSX.Element => { +const Dashboard = (): JSX.Element => { const [loginModal, setLoginModal] = useState(false); const [client, setClient] = useState(); const { @@ -43,47 +41,18 @@ const Dashboard = ({ val }: { val?: string }): JSX.Element => { loading: loadingUser, error: userError, } = useSelector((state: RootState) => state.userState); - const [selectedOption, setSelectedOption] = useState(1); - const [unreadMessages, setUnreadMsg] = useState(0); - const [showMessageBox, setShowMessageBox] = useState(false); - const [targetUser, setTargetUser] = useState(null); - const [myApplications, _setMyApplications] = useState(); const [loadingStreamChat, setLoadingStreamChat] = useState(true); - const router = useRouter(); - const { briefId } = router.query; const [error, setError] = useState(userError); const dispatch = useDispatch(); - const handleMessageBoxClick = async ( - user_id: number, - _freelancer: Freelancer - ) => { - if (user_id) { - setShowMessageBox(true); - setTargetUser(await fetchUser(user_id)); - } else { - //TODO: check if user is logged in - // redirect("login", `/dapp/freelancers/${freelancer?.username}/`); - } - }; - - const redirectToBriefApplications = (applicationId: string) => { - router.push(`/briefs/${briefId}/applications/${applicationId}`); - }; - - useEffect(() => { - if (val === 'message') setSelectedOption(2); - }, [val]); - useEffect(() => { const setupStreamChat = async () => { try { if (!user?.username && !loadingUser) return router.push('/'); setClient(await getStreamChat()); - _setMyApplications(await getFreelancerApplications(user?.id)); } catch (error) { setError({ message: error }); } finally { @@ -110,67 +79,23 @@ const Dashboard = ({ val }: { val?: string }): JSX.Element => { }; getUnreadMessageChannels(); client.on((event) => { - console.log(event); if (event.total_unread_count !== undefined) { dispatch(setUnreadMessage({ message: event.unread_channels })); - setUnreadMsg(event.total_unread_count); } }); } }, [client, user?.getstream_token, user?.username, loadingStreamChat]); + const { profileView } = useContext(AppContext) as AppContextType; + if (loadingStreamChat || loadingUser) return ; + if (profileView === 'freelancer') return ; + return client ? (
- - { - router.push('/dashboard'); - setSelectedOption(newValue); - }} - > - - 0 ? `(${unreadMessages})` : '' - }`} - value={2} - /> - - - - - {selectedOption === 1 && ( - - )} - {selectedOption === 2 && } - {selectedOption === 3 && ( - - )} - - {user && showMessageBox && ( - - )} - + + state.userState); + const router = useRouter(); + const [client, setClient] = useState(); + const [loadingStreamChat, setLoadingStreamChat] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + const setupStreamChat = async () => { + try { + if (!user?.username && !loadingUser) return router.push('/'); + setClient(await getStreamChat()); + } catch (error) { + setError({ message: error }); + } finally { + setLoadingStreamChat(false); + } + }; + setupStreamChat(); + }, [user.id]); + + useEffect(() => { + if (client && user?.username && !loadingStreamChat) { + client?.connectUser( + { + id: String(user.id), + username: user.username, + name: user.display_name, + }, + user.getstream_token + ); + } + }, [user.username, client, user.id, user.display_name, user.getstream_token, loadingStreamChat]); + + if (loadingStreamChat || loadingUser || !client) { + return ; + } + + if (error) { + router.push('/'); + } + + return ( +
+ +
+ ); +} diff --git a/src/pages/dashboard/new.tsx b/src/pages/dashboard/new.tsx new file mode 100644 index 00000000..c78cd8f9 --- /dev/null +++ b/src/pages/dashboard/new.tsx @@ -0,0 +1,429 @@ +/* eslint-disable unused-imports/no-unused-vars */ +import { Badge } from '@mui/material'; +import { useRouter } from 'next/router'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { BiChevronDown, BiRightArrowAlt } from 'react-icons/bi'; +import { MdOutlineAttachMoney } from 'react-icons/md'; +import { useDispatch, useSelector } from 'react-redux'; +import { + DefaultGenerics, + FormatMessageResponse, + StreamChat, +} from 'stream-chat'; +import 'stream-chat-react/dist/css/v2/index.css'; + +import { fetchUser, getStreamChat } from '@/utils'; + +import ChatPopup from '@/components/ChatPopup'; +import BriefsView from '@/components/Dashboard/V2/BriefsView'; +import FullScreenLoader from '@/components/FullScreenLoader'; +import { AppContext, AppContextType } from '@/components/Layout'; +import MessageComponent from '@/components/MessageComponent'; + +import { Project, User } from '@/model'; +import { Brief } from '@/model'; +import { + getFreelancerApplications, + getFreelancerProfile, +} from '@/redux/services/freelancerService'; +import { setUnreadMessage } from '@/redux/slices/userSlice'; +import { RootState } from '@/redux/store/store'; + +export type DashboardProps = { + user: User; + isAuthenticated: boolean; + myBriefs: Brief; + myApplicationsResponse: Project[]; +}; + +const FreelancerDashboard = (): JSX.Element => { + const [client, setClient] = useState(); + const { + user, + loading: loadingUser, + error: userError, + } = useSelector((state: RootState) => state.userState); + const [unreadMessages, setUnreadMsg] = useState(0); + const [showMessageBox, setShowMessageBox] = useState(false); + const [targetUser, setTargetUser] = useState(null); + const [loadingStreamChat, setLoadingStreamChat] = useState(true); + + const [messageList, setMessageList] = useState< + FormatMessageResponse[] | null + >(); + + const router = useRouter(); + + const [error, setError] = useState(userError); + + const dispatch = useDispatch(); + + const handleMessageBoxClick = async (user_id: number) => { + if (user_id) { + setShowMessageBox(true); + setTargetUser(await fetchUser(user_id)); + } else { + //TODO: check if user is logged in + // redirect("login", `/dapp/freelancers/${freelancer?.username}/`); + } + }; + + const { setProfileMode } = useContext(AppContext) as AppContextType; + + useEffect(() => { + setLoadingStreamChat(true); + + const checkFreelancerProfile = async () => { + try { + const freelancer = await getFreelancerProfile(user?.username); + if (!freelancer?.id) { + setProfileMode('client'); + router.push('/freelancers/new'); + } + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + setLoadingStreamChat(false); + } + }; + checkFreelancerProfile(); + }, [loadingUser, router, setProfileMode, user?.username]); + + useEffect(() => { + const setupStreamChat = async () => { + try { + if (!user?.username && !loadingUser) return router.push('/'); + setClient(await getStreamChat()); + } catch (error) { + setError({ message: error }); + } finally { + setLoadingStreamChat(false); + } + }; + + setupStreamChat(); + }, [loadingUser, router, user?.id, user?.username]); + + useEffect(() => { + if (client && user?.username && !loadingStreamChat) { + client?.connectUser( + { + id: String(user.id), + username: user.username, + name: user.display_name, + }, + user.getstream_token + ); + const getUnreadMessageChannels = async () => { + const result = await client.getUnreadCount(); + dispatch(setUnreadMessage({ message: result.channels.length })); + }; + + const getChannel = async () => { + const filter = { + type: 'messaging', + members: { $in: [String(user.id)] }, + }; + const sort: any = { last_message_at: -1 }; + const channels = await client.queryChannels(filter, sort, { + limit: 4, + watch: true, // this is the default + state: true, + }); + const lastMessages: FormatMessageResponse[] = []; + channels.map((channel) => { + lastMessages.push(channel.lastMessage()); + }); + setMessageList(lastMessages); + }; + getUnreadMessageChannels(); + getChannel(); + client.on((event) => { + if (event.total_unread_count !== undefined) { + getChannel(); + dispatch(setUnreadMessage({ message: event.unread_channels })); + setUnreadMsg(event.total_unread_count); + } + }); + } + }, [client, user?.getstream_token, user?.username, loadingStreamChat]); + + const [applications, setApplciations] = useState([]); + + const completedProjects = applications.filter( + (app) => app.completed === true + ); + + const totalEarnings = useMemo(() => { + const initValue = 0; + const total = completedProjects.reduce( + (acc, curr) => acc + Number(curr.total_cost_without_fee), + initValue + ); + return total; + }, [completedProjects]); + + const activeProjects = applications.filter( + (app) => app.completed === false && app.chain_project_id && app.brief_id + ); + const pendingProjects = applications.filter((app) => app.status_id === 1); + const grants = applications.filter( + (app) => !app.brief_id && app.chain_project_id + ); + + useEffect(() => { + const getProjects = async () => { + const applications = await getFreelancerApplications(user.id); + + setApplciations(applications); + }; + + if (user?.id) getProjects(); + }, [user?.id]); + + const options = [ + { name: 'Approved', bg: 'bg-[#90DB00]', status_id: 4 }, + { name: 'Pending', bg: 'bg-[#FF7A00]', status_id: 1 }, + { name: 'Changes Required', bg: 'bg-[#3B27C1]', status_id: 2 }, + { name: 'Rejected', bg: 'bg-[#FF7A00]', status_id: 3 }, + ]; + + const [selectedOption, setSelectedOption] = useState<(typeof options)[0]>( + options[0] + ); + const [openedOption, setOpenedOption] = useState(false); + const [filteredApplications, setFilteredApplications] = useState(); + + useEffect(() => { + const getProjects = async () => { + // setLoading(true) + + try { + const Briefs = await getFreelancerApplications(user.id); + + const projectRes = Briefs.filter( + (item) => + item.status_id == selectedOption.status_id && !item.chain_project_id + ); + setFilteredApplications(projectRes); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + // setLoading(false) + } + }; + + if (user?.id) getProjects(); + }, [selectedOption.status_id, user?.id]); + + if (loadingStreamChat || loadingUser) return ; + + return client ? ( +
+ <> +

+ Welcome, {user?.display_name?.split(' ')[0]} 👋 +

+

+ Glad to have you on imbue +

+ + {/* starting of the box sections */} +
+
+
+

Projects

+ +

router.push('/projects/myprojects')} + > + View all +

+
+
+
+
+

Completed Projects

+
+

+ {completedProjects?.length || 0} +

+
+
+
+
+
+

Active Projects

+
+

+ {activeProjects?.length || 0} +

+
+
+
+
+
+

Pending Projects

+
+

+ {pendingProjects?.length || 0} +

+
+
+
+
+
+

Grants

+
+

+ {grants?.length || 0} +

+
+
+
+
+
+

Briefs

+
+
setOpenedOption((prev) => !prev)} + > +
{' '} +

{selectedOption.name}

+ +
+ +
+ {options.map((option, index) => ( +
{ + setSelectedOption(option); + setOpenedOption(false); + }} + > +
+

{option.name}

+
+ ))} +
+
+
+
+
+

+ {filteredApplications?.length} +

+
+
+

{selectedOption.name} brief

+
+ router.push( + `/projects/applications?status_id=${selectedOption.status_id}` + ) + } + > + +
+
+
+
+
+
+

Total Earnings

+
router.push('/relay')} + className='px-3 py-0.5 border cursor-pointer text-black border-text-aux-colour rounded-full' + > + +
+
+
+
+ +

{totalEarnings}

+
+

Payout Accounts

+
+
+
+ {/* ending of the box sections */} +
+ +
+ {/* Starting of graph */} + {/*
+
+

Analytics

+ +
+
+

+ 124 +

+ +
+
*/} + {/* End of graph */} +
+
+ +

Messaging

+
+
router.push('/dashboard/messages')} + > + +
+
+
+ {messageList?.map((item, index) => ( + + ))} +
+
+
+
+ {user && showMessageBox && ( + + )} +
+ ) : ( +

GETSTREAM_API_KEY not found

+ ); +}; + +export default FreelancerDashboard; diff --git a/src/pages/freelancers/new.tsx b/src/pages/freelancers/new.tsx index 66ea0dfa..13b77c13 100644 --- a/src/pages/freelancers/new.tsx +++ b/src/pages/freelancers/new.tsx @@ -2,7 +2,7 @@ import { Autocomplete, TextField, Tooltip } from '@mui/material'; import Filter from 'bad-words'; import { useRouter } from 'next/router'; -import React, { ChangeEvent, useEffect, useState } from 'react'; +import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import * as utils from '@/utils'; @@ -10,6 +10,7 @@ import { isUrlExist, validateInputLength } from '@/utils/helper'; import ErrorScreen from '@/components/ErrorScreen'; import FullScreenLoader from '@/components/FullScreenLoader'; +import { AppContext, AppContextType } from '@/components/Layout'; import ValidatableInput from '@/components/ValidatableInput'; import { @@ -55,6 +56,7 @@ const Freelancer = (): JSX.Element => { ); const [suggestedLanguages, setSuggestedLanguages] = useState([]); const dispatch = useDispatch(); + const { setProfileMode } = useContext(AppContext) as AppContextType useEffect(() => { if (!userLoading && (!user || !user?.id)) { @@ -126,9 +128,8 @@ const Freelancer = (): JSX.Element => {
setFreelancingBefore(value)} > {label} @@ -150,9 +151,8 @@ const Freelancer = (): JSX.Element => {
setGoal(value)} > {label} @@ -466,6 +466,7 @@ const Freelancer = (): JSX.Element => { if (response.status === 201) { dispatch(fetchUserRedux()); + setProfileMode('freelancer') setStep(step + 1); } else { setError({ @@ -525,10 +526,9 @@ const Freelancer = (): JSX.Element => { } >
)} - +
{openNoRefundList && ( @@ -836,9 +842,8 @@ function Project() {
Grant Wallet Address Copied to clipboard diff --git a/src/pages/projects/applications.tsx b/src/pages/projects/applications.tsx new file mode 100644 index 00000000..70ee0ee4 --- /dev/null +++ b/src/pages/projects/applications.tsx @@ -0,0 +1,62 @@ +import ArrowBackIcon from '@mui/icons-material/ChevronLeft'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import BreifApplication from '@/components/Dashboard/FreelacerView/BriefApplication/BreifApplication'; +import FullScreenLoader from '@/components/FullScreenLoader'; + +import { applicationStatusId, Project } from '@/model'; +import { getFreelancerApplications } from '@/redux/services/freelancerService'; +import { RootState } from '@/redux/store/store'; + +export default function Applications() { + const { user, loading: loadingUser } = useSelector( + (state: RootState) => state.userState + ); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + + const router = useRouter() + const { status_id } = router.query + + useEffect(() => { + const getProjects = async () => { + setLoading(true) + + try { + const Briefs = await getFreelancerApplications(user.id); + + const projectRes = Briefs.filter((item) => item.status_id == status_id && !item.chain_project_id); + setProjects(projectRes); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + } finally { + setLoading(false) + } + }; + + if (user?.id) getProjects(); + }, [status_id, user.id]); + + if (loadingUser || loading) { + return ; + } + return ( +
+
router.back()} + className='border border-content group hover:bg-content rounded-full flex items-center justify-center cursor-pointer absolute left-5 top-10' + > + +
+
+

{applicationStatusId[Number(status_id)]} Projects

+
+
+ +
+
+ ); +} diff --git a/src/pages/projects/myprojects.tsx b/src/pages/projects/myprojects.tsx new file mode 100644 index 00000000..a1d33d4d --- /dev/null +++ b/src/pages/projects/myprojects.tsx @@ -0,0 +1,203 @@ +import ArrowBackIcon from '@mui/icons-material/ChevronLeft'; +import { Divider } from '@mui/material'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en'; +import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { ProgressBar } from '@/components/ProgressBar'; + +import { applicationStatusId, Project } from '@/model'; +import { getFreelancerApplications } from '@/redux/services/freelancerService'; +import { RootState } from '@/redux/store/store'; + +TimeAgo.addLocale(en); + +const timeAgo = new TimeAgo('en-US'); + +export default function Myprojects() { + const [switcher, setSwitcher] = useState('completed'); + const { user, loading: loadingUser } = useSelector( + (state: RootState) => state.userState + ); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [allProject, setAllProjects] = useState([]); + + const completedProject = useMemo(() => { + return allProject.filter((item) => item.completed); + }, [allProject]); + + const activeProject = useMemo(() => { + return allProject.filter( + (item) => item.chain_project_id && !item.completed && item.brief_id + ); + }, [allProject]); + + const pendingProject = useMemo(() => { + return allProject.filter( + (item) => !item.chain_project_id && item.status_id === 4 + ); + }, [allProject]); + + const GrantProject = useMemo(() => { + return allProject.filter((item) => !item.brief_id && item.status_id === 4); + }, [allProject]); + + useEffect(() => { + const getProjects = async () => { + setLoading(true); + + try { + const projects = await getFreelancerApplications(user.id); + setAllProjects(projects); + + switch (switcher) { + case 'completed': { + setProjects(completedProject); + break; + } + case 'active': { + setProjects(activeProject); + break; + } + case 'pending': { + setProjects(pendingProject); + break; + } + case 'grants': { + setProjects(GrantProject); + break; + } + } + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + } finally { + setLoading(false); + } + }; + + if (user?.id) getProjects(); + }, [switcher, user.id]); + + const router = useRouter(); + + const redirectToApplication = (project: Project) => { + router.push(`/projects/${project.id}`); + }; + + return ( +
+
router.back()} + className='border border-content group hover:bg-content rounded-full flex items-center justify-center cursor-pointer absolute left-5 top-10' + > + +
+ +
+

setSwitcher('completed')} + className='text-2xl text-black py-5 border-r text-center w-full ' + > + Completed Projects({completedProject?.length || 0}) +

+

setSwitcher('active')} + className='text-2xl text-black py-5 border-r text-center w-full' + > + Active Projects ({activeProject?.length || 0}) +

+

setSwitcher('pending')} + className='text-2xl text-black border-r py-5 text-center w-full' + > + Pending Projects ({pendingProject?.length || 0}) +

+

setSwitcher('grants')} + className='text-2xl text-black py-5 text-center w-full' + > + Grants ({GrantProject?.length || 0}) +

+
+
+ {(loadingUser || loading) && ( +

Loading...

+ )} + {!(loadingUser || loading) && + projects.map((project, index) => ( + <> +
redirectToApplication(project)} + className=' hover:bg-imbue-light-purple cursor-pointer px-9 text-imbue-purple' + > +
+
+ {project.milestones && ( + <> +
+ it.is_approved === true + ).length + } + /> +
+

+ { + project.milestones?.filter( + (it: any) => it.is_approved === true + ).length + } + /{project.milestones?.length} +

+ + )} + {project.status_id && ( + + )} +
+

+ {project.name} +

+

+ {timeAgo?.format(new Date(project?.created || 0))} +

+
+
+

+ {project.description} +

+
+
+
+

${project.required_funds}

+

Fixed price

+
+
+
+ {index !== projects.length - 1 && } + + ))} +
+
+ ); +} diff --git a/src/pages/projects/ongoing.tsx b/src/pages/projects/ongoing.tsx new file mode 100644 index 00000000..df12ac29 --- /dev/null +++ b/src/pages/projects/ongoing.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import OngoingProject from '@/components/Dashboard/FreelacerView/OngoingProject/OngoingProject'; +import FullScreenLoader from '@/components/FullScreenLoader'; + +import { Project } from '@/model'; +import { getFreelancerApplications } from '@/redux/services/freelancerService'; +import { RootState } from '@/redux/store/store'; + +export default function Ongoing() { + const { user, loading: loadingUser } = useSelector( + (state: RootState) => state.userState + ); + const [projects, setProjects] = useState([]); + + useEffect(() => { + const getProjects = async () => { + const Briefs = await getFreelancerApplications(user.id); + + const projectRes = Briefs.filter((item) => item.chain_project_id); + setProjects(projectRes); + }; + + if (user?.id) getProjects(); + }, [user.id]); + + if (loadingUser) { + return ; + } + return ( +
+
+

Ongoing Projects

+
+
+ +
+
+ ); +} diff --git a/src/pages/relay/index.tsx b/src/pages/relay/index.tsx index 2e022cd0..626b4503 100644 --- a/src/pages/relay/index.tsx +++ b/src/pages/relay/index.tsx @@ -1,3 +1,4 @@ +import ArrowBackIcon from '@mui/icons-material/ChevronLeft'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import { FilledInput, @@ -8,7 +9,7 @@ import { MenuItem, } from '@mui/material'; import { Signer } from '@polkadot/api/types'; -import { decodeAddress } from "@polkadot/util-crypto/address" +import { decodeAddress } from '@polkadot/util-crypto/address'; import { WalletAccount } from '@talismn/connect-wallets'; import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; @@ -28,44 +29,50 @@ import { RootState } from '@/redux/store/store'; const Currencies = [ { - name: "IMBU", - currencyId: 0 + name: 'IMBU', + currencyId: 0, }, { - name: "KSM", - currencyId: 1 + name: 'KSM', + currencyId: 1, }, // {AUSD : 2}, // {KAR : 3}, { - name: "MGX", - currencyId: 4 + name: 'MGX', + currencyId: 4, }, -] +]; const Relay = () => { const [transferAmount, setTransferAmount] = useState(0); - const [showPolkadotAccounts, setShowPolkadotAccounts] = useState(false) + const [showPolkadotAccounts, setShowPolkadotAccounts] = + useState(false); // screens - const [error, setError] = useState() - const [success, setSuccess] = useState(false) - const [loading, setLoading] = useState(false) + const [error, setError] = useState(); + const [success, setSuccess] = useState(false); + const [loading, setLoading] = useState(false); - const router = useRouter() + const router = useRouter(); const transferFromChain = async (account: WalletAccount) => { setShowPolkadotAccounts(false); setLoading(true); - const relayApi = (await initImbueAPIInfo()).relayChain.api - const imbueApi = (await initImbueAPIInfo()).imbue.api - const transferAmountInt = BigInt(parseFloat(transferAmount.toString()) * 1e12); + const relayApi = (await initImbueAPIInfo()).relayChain.api; + const imbueApi = (await initImbueAPIInfo()).imbue.api; + const transferAmountInt = BigInt( + parseFloat(transferAmount.toString()) * 1e12 + ); if (relayApi && transferAmountInt) { // Todo: loading screen - const { data: { free: freeBalance } } = await relayApi.query.system.account(account.address) as any - const userHasEnoughBalance = freeBalance.toBigInt() >= Number(transferAmount) + const { + data: { free: freeBalance }, + } = (await relayApi.query.system.account(account.address)) as any; + const userHasEnoughBalance = + freeBalance.toBigInt() >= Number(transferAmount); if (userHasEnoughBalance) { const dest = { V3: { @@ -81,23 +88,32 @@ const Relay = () => { interior: { X1: { AccountId32: { - id: decodeAddress(account.address) - } - } - } - } + id: decodeAddress(account.address), + }, + }, + }, + }, }; const assets = { - V3: [{ - id: { Concrete: { parents: 0, interior: "Here" } }, - fun: { Fungible: 1000000000000 } - }] + V3: [ + { + id: { Concrete: { parents: 0, interior: 'Here' } }, + fun: { Fungible: 1000000000000 }, + }, + ], }; const feeAssetItem = 0; const weightLimit = 'Unlimited'; - const extrinsic = await relayApi?.tx.xcmPallet.limitedReserveTransferAssets(dest, beneficiary, assets, feeAssetItem, weightLimit); + const extrinsic = + await relayApi?.tx.xcmPallet.limitedReserveTransferAssets( + dest, + beneficiary, + assets, + feeAssetItem, + weightLimit + ); try { await extrinsic.signAndSend( account.address, @@ -105,67 +121,67 @@ const Relay = () => { (result) => { imbueApi?.query.system.events((events: any) => { if (events) { - if (!result || !result.status || !events) { return; } - // Loop through the Vec events.forEach((record: any) => { const { event } = record; - const currenciesDeposited = `${event.section}.${event.method}` == "ormlTokens.Deposited"; + const currenciesDeposited = + `${event.section}.${event.method}` == + 'ormlTokens.Deposited'; if (currenciesDeposited) { // const types = event.typeDef; const accountId = event.data[1]; if (accountId == account.address) { - setSuccess(true) + setSuccess(true); } } }); } }); - }); - + } + ); } catch (error: any) { // eslint-disable-next-line no-console - console.error(error) - setError({ message: "Error occurred. " + error }) + console.error(error); + setError({ message: 'Error occurred. ' + error }); } finally { - setLoading(false) + setLoading(false); } - } - else { - const avilableBalance = Number(freeBalance.toBigInt() / BigInt(1e12)) - const errorMessage = `Error: Insuffient balance to complete transfer. Available balance is ${avilableBalance.toFixed(2)}` + } else { + const avilableBalance = Number(freeBalance.toBigInt() / BigInt(1e12)); + const errorMessage = `Error: Insuffient balance to complete transfer. Available balance is ${avilableBalance.toFixed( + 2 + )}`; // eslint-disable-next-line no-console - console.error(errorMessage) - setError({ message: errorMessage }) + console.error(errorMessage); + setError({ message: errorMessage }); } } - } + }; const { user, loading: loadingUser } = useSelector( (state: RootState) => state.userState ); - const [balanceLoading, setBalanceLoading] = useState(true) - const [requestSent, setRequestSent] = useState(false) - const [currency_id, setCurrency_id] = useState(0) - const [balance, setBalance] = useState(0) + const [balanceLoading, setBalanceLoading] = useState(true); + const [requestSent, setRequestSent] = useState(false); + const [currency_id, setCurrency_id] = useState(0); + const [balance, setBalance] = useState(0); const [anchorEl, setAnchorEl] = React.useState(null); const showOptions = Boolean(anchorEl); - // useEffect(() => { // getAndSetBalace() // }, [currency_id, user.id]) useEffect(() => { const getAndSetBalace = async () => { - if (!requestSent && !loadingUser && !user?.web3_address) return + if (!requestSent && !loadingUser && !user?.web3_address) return; - setRequestSent(true) + setRequestSent(true); try { const balance = await getBalance( user.web3_address as string, @@ -178,19 +194,17 @@ const Relay = () => { // eslint-disable-next-line no-console console.error(error); } finally { - setBalanceLoading(false) - setRequestSent(false) + setBalanceLoading(false); + setRequestSent(false); } - } + }; const timer = setInterval(() => { - getAndSetBalace() + getAndSetBalace(); }, 5000); return () => clearInterval(timer); - }, [balanceLoading, currency_id, loadingUser, requestSent, user]); - const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -198,57 +212,64 @@ const Relay = () => { setAnchorEl(null); }; const selectCurrency = (currencyID: number) => { - setCurrency_id(currencyID) + setCurrency_id(currencyID); setAnchorEl(null); }; return (
-

My funds

+
+
router.back()} + className='border border-content group hover:bg-content rounded-full flex items-center justify-center cursor-pointer left-5 top-10' + > + +
+

My funds

+

Transfer KSM to Imbue Network

- {balanceLoading - ? ( -

Loading Balance...

) - : ( -
-
- - - { - Currencies.map((currency) => ( - selectCurrency(currency.currencyId)} - > - {currency.name} - - )) - } - -
-

- Balance : {balance} ${Currency[currency_id || 0]} -

+ {balanceLoading ? ( +

Loading Balance...

+ ) : ( +
+
+ + + {Currencies.map((currency) => ( + selectCurrency(currency.currencyId)} + > + {currency.name} + + ))} +
- ) - } +

+ Balance : {balance} ${Currency[currency_id || 0]} +

+
+ )}
{ />