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
+
+ )}
+
+
+
+
+
+
+
{
+ 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 (
+
+
+
+
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 (
+
+
+
+
+
{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 (
<>
- {user?.id
- ? (
-
-
-
- Submit
-
-
+ {user?.id ? (
+
+
+
+ Submit
+
+
-
-
-
{
- router.push('/grants/new');
- }}
- className='flex gap-2 px-2 hover:bg-imbue-lime-light items-center py-2 rounded-md '
- >
-
-
-
-
+
+
+
{
+ router.push('/grants/new');
+ }}
+ className='flex gap-2 px-2 hover:bg-imbue-lime-light items-center 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 '
+ >
+
+
-
{
- router.push('/briefs/new');
- }}
- className='flex gap-2 px-2 items-center hover:bg-imbue-lime-light py-2 rounded-md '
- >
-
-
-
-
+
-
)
- : ""
- }
+
+
+ ) : (
+ ''
+ )}
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='#'
>
-
-
-
-
-
-
+
+
+ {profileView === 'client' ? (
+
{
+ setProfileMode('freelancer')
+ router.push('/dashboard')
+ }}
+ >
+
+
+
+
+
+ ) : (
+
{
+ setProfileMode('client')
+ router.push('/dashboard')
+ }}
+ >
+
+
+
+
+
+ )}
- */}
+
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 (
+
+ );
+};
+
+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
+
+
+
+
+
+
+
+
+
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'
+ >
+
+
+
+
+
+
+ {/* ending of the box sections */}
+
+
+
+ {/* Starting of graph */}
+ {/*
*/}
+ {/* 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 => {
}
>