diff --git a/public/Onboard/bank-note-01.svg b/public/Onboard/bank-note-01.svg new file mode 100644 index 00000000..d12e9627 --- /dev/null +++ b/public/Onboard/bank-note-01.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Onboard/building-02.svg b/public/Onboard/building-02.svg new file mode 100644 index 00000000..fbfc691b --- /dev/null +++ b/public/Onboard/building-02.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Onboard/globe-05.svg b/public/Onboard/globe-05.svg new file mode 100644 index 00000000..c5b398a8 --- /dev/null +++ b/public/Onboard/globe-05.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Onboard/hearts.svg b/public/Onboard/hearts.svg new file mode 100644 index 00000000..0ab63b91 --- /dev/null +++ b/public/Onboard/hearts.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Onboard/safe.svg b/public/Onboard/safe.svg new file mode 100644 index 00000000..7b047224 --- /dev/null +++ b/public/Onboard/safe.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/Onboard/zap-square.svg b/public/Onboard/zap-square.svg new file mode 100644 index 00000000..1fa8cf10 --- /dev/null +++ b/public/Onboard/zap-square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/AreaGraph/index.tsx b/src/components/AreaGraph/index.tsx index 76461928..33def241 100644 --- a/src/components/AreaGraph/index.tsx +++ b/src/components/AreaGraph/index.tsx @@ -1,10 +1,16 @@ import dynamic from 'next/dynamic'; +import { userAnalyticsType } from '@/pages/dashboard/new'; + const Chart = dynamic(() => import('react-apexcharts'), { ssr: false, }); -export default function AreaGrah() { +export default function AreaGrah({ + userInfo, +}: { + userInfo: userAnalyticsType[]; +}) { const options = { fill: { colors: ['#3B27C1'], @@ -21,14 +27,14 @@ export default function AreaGrah() { show: false, }, xaxis: { - categories: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + categories: userInfo?.map((item) => item.label.substring(0, 3)), }, }; const series = [ { name: 'profile views', - data: [0, 0, 3, 5, 2, 6, 0], + data: userInfo?.map((item) => item.value), }, ]; diff --git a/src/components/BriefComponent/index.tsx b/src/components/BriefComponent/index.tsx index b87b8582..fdfdfdc4 100644 --- a/src/components/BriefComponent/index.tsx +++ b/src/components/BriefComponent/index.tsx @@ -1,9 +1,8 @@ -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 { useEffect, useState } from 'react'; import { AiOutlineCalendar, AiOutlineClockCircle, @@ -15,13 +14,28 @@ 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 }) { +export default function BriefComponent({ brief }: { brief: any }) { const router = useRouter(); + const [user_Activites, setUserActivites] = useState({ + totalSpent: 0, + totalHire: 0, + activeHire: 0, + }); + useEffect(() => { + let totalSpent = 0; + let totalHire = 0; + let activeHire = 0; + for (const project of brief.user_hire_history) { + if (project.project_status === 6) totalSpent += Number(project.cost); + if (project.project_status === 4 || project.status === 5) activeHire++; + if (project.project_status !== 0) totalHire++; + } + setUserActivites({ totalSpent, activeHire, totalHire }); + }, [brief]); + return (
- $19k total spent + $ + {user_Activites.totalSpent >= 1000 + ? Math.trunc(user_Activites.totalSpent / 1000) + 'k' + : user_Activites.totalSpent}{' '} + total spent

- 59 hires,6 active + {user_Activites.totalHire} hires,{user_Activites.activeHire} active

- } - /> + /> */}
-

4.68 of 40 reviews

-

Member since: Aug 17,2023

+ {/*

4.68 of 40 reviews

*/} +

Member since: {timeAgo.format(new Date(brief.joined))}

diff --git a/src/components/FreelancerCard/FreelancerCard.tsx b/src/components/FreelancerCard/FreelancerCard.tsx index 87f761f3..fc0d6b9f 100644 --- a/src/components/FreelancerCard/FreelancerCard.tsx +++ b/src/components/FreelancerCard/FreelancerCard.tsx @@ -1,19 +1,34 @@ import Image from 'next/image'; import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; 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; + freelancer: any; handleMessage: any; }) { const router = useRouter(); + const [freelancerSuccsRate, setFreelancerSuccessRate] = useState(0); + + useEffect(() => { + let cancelProject = 0; + let completedProject = 0; + for (const project of freelancer.projects) { + if (project.status_id === 6) completedProject++; + if (project.status_id === 3) cancelProject++; + } + if (completedProject && cancelProject) { + const success = + (completedProject / (completedProject + cancelProject)) * 100; + setFreelancerSuccessRate(success); + } + }, [freelancer]); + return (
@@ -44,14 +59,23 @@ export default function FreelancerCard({

- {/*
+

- $50-$75 hr + ${freelancer.hour_per_rate.toFixed(2)} + hr

- Job Success rate 99.2% + Job Success rate{' '} + {freelancerSuccsRate > 0 && ( + + {Math.floor(freelancerSuccsRate)}% + + )} + {freelancerSuccsRate === 0 && ( + NA + )}

-
*/} +
{[1, 2, 3, 4].map( (item: number, index: number) => diff --git a/src/components/MessageComponent/index.tsx b/src/components/MessageComponent/index.tsx index 93297f41..ca3ec2d8 100644 --- a/src/components/MessageComponent/index.tsx +++ b/src/components/MessageComponent/index.tsx @@ -1,7 +1,11 @@ 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'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Channel, DefaultGenerics, User } from 'stream-chat'; + +import { RootState } from '@/redux/store/store'; TimeAgo.addLocale(en); @@ -11,15 +15,22 @@ export default function MessageComponent({ handleMessageClick, props, }: { - props: FormatMessageResponse; + props: Channel; handleMessageClick: any; }) { + const { user } = useSelector((state: RootState) => state.userState); + + const [targetUser, setTargetUser] = useState(); + + useEffect(() => { + const key = Object.keys(props.state.members); + Number(props.state.members[key[0]]?.user_id) === user.id + ? setTargetUser(props.state.members[key[1]]?.user) + : setTargetUser(props.state.members[key[0]]?.user); + }, [props.state.members, user.id]); + const handleClick = () => { - // router.push({ - // pathname: `/dashboard/messages`, - // query: `chat=${props.cid?.split(':')[1]}`, - // }); - handleMessageClick(props?.user?.id); + handleMessageClick(targetUser?.id); }; return ( @@ -28,9 +39,9 @@ export default function MessageComponent({ className='flex items-center hover:bg-imbue-light-purple-three px-4 py-1 rounded-sm cursor-pointer text-black gap-5 text-sm' >
-

{props?.user?.name}

-

{props?.text}

+

{targetUser?.name}

+

{props?.lastMessage()?.text}

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

diff --git a/src/components/Navbar/NewNavBar.tsx b/src/components/Navbar/NewNavBar.tsx index 71ff4a62..1aae663f 100644 --- a/src/components/Navbar/NewNavBar.tsx +++ b/src/components/Navbar/NewNavBar.tsx @@ -60,8 +60,6 @@ function NewNavbar() { } = useSelector((state: RootState) => state.userState); const dispatch = useDispatch(); - const [expanded, setExpanded] = useState(false); - const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); setOpenMenu(Boolean(event.currentTarget)); @@ -89,18 +87,12 @@ function NewNavbar() { setup(); }, [dispatch, user?.username]); - const navigateToPage = (url: string) => { - if (user?.username) { - router.push(url); - } else { - setLoginModal(true); - } - }; - 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 + const { profileView, setProfileMode } = useContext( + AppContext + ) as AppContextType; return ( <> @@ -242,7 +234,6 @@ function NewNavbar() { '' )} 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`} href='/relay' > @@ -256,7 +247,6 @@ 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}`} > { - setProfileMode('freelancer') - router.push('/dashboard') + setProfileMode('freelancer'); + router.push('/dashboard'); }} >
@@ -292,8 +282,8 @@ function NewNavbar() {
{ - setProfileMode('client') - router.push('/dashboard') + setProfileMode('client'); + router.push('/dashboard'); }} >
@@ -420,13 +410,6 @@ function NewNavbar() { /> - {/* { - setLoginModal(val); - }} - redirectUrl={router?.asPath} - /> */} { diff --git a/src/components/WelcomeModalContent/WelcomeForNewUser.tsx b/src/components/WelcomeModalContent/WelcomeForNewUser.tsx new file mode 100644 index 00000000..0386896b --- /dev/null +++ b/src/components/WelcomeModalContent/WelcomeForNewUser.tsx @@ -0,0 +1,128 @@ +import classNames from 'classnames'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { AiOutlineArrowUp } from 'react-icons/ai'; +import { useSelector } from 'react-redux'; + +import { RootState } from '@/redux/store/store'; + +export default function WelcomForNewUser({ + handleClose, +}: { + handleClose: any; +}) { + const { user } = useSelector((state: RootState) => state.userState); + const [select, setSelected] = useState('work'); + const router = useRouter(); + const handleDirection = () => { + if (select === 'work') { + router.push('/freelancers/new'); + } + handleClose(false); + }; + + return ( +
+
+

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

+

Glad to have you join

+
+
+
+

+ icons + Get paid securely +

+

+ icons + Work on what you love +

+

+ icons + Build your dream team +

+
+ +
+
+
+

+ icons + Build,Ship,Launch Fast! +

+

+ icons + Saftey & Transparency +

+

+ icons + Get a world-class teams +

+
+ +
+
+

+ You can always switch/create a hirer or freelancer profile after you + sign up +

+
+ +
+ ); +} diff --git a/src/config/freelancer-data.ts b/src/config/freelancer-data.ts index 50256deb..41d4bbd0 100644 --- a/src/config/freelancer-data.ts +++ b/src/config/freelancer-data.ts @@ -39,10 +39,10 @@ export const stepData = [ // progress: 6, // }, { - heading: "Clients like to know what you know - add your education here.", - content: `You don’t have to have a degree. Adding any relevant education helps \n + heading: 'Clients like to know what you know - add your education here.', + content: `You don’t have to have a degree. Adding any relevant education helps \n make your profile more visible.`, - progress: 7, + progress: 7, }, { heading: 'Looking good. Next, tell us which languages you speak.', @@ -70,10 +70,16 @@ export const stepData = [ Search for a service`, progress: 11, }, + { + heading: 'What is your expected payment per hour?', + content: `Choose how much you would like to charge for your service per hour rate. This \n + will clarify the clients about how much they should pay you for your services\n`, + progress: 12, + }, { heading: "That's It. All done!", content: `Once you submit your profile you will be able to search and apply for briefs`, - progress: 12, + progress: 13, }, ]; diff --git a/src/lib/models.ts b/src/lib/models.ts index df735d7e..fb904f30 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -194,6 +194,7 @@ export type Freelancer = { country?: string; region?: string; profile_image?: string; + hour_per_rate: number; }; export type BriefSqlFilter = { @@ -523,6 +524,22 @@ export const insertProject = (project: Project) => async (tx: Knex.Transaction) => (await tx('projects').insert(project).returning('*'))[0]; +export const insertUserAnalytics = + (user_analytics: any) => async (tx: Knex.Transaction) => + (await tx('user_analytic').insert(user_analytics).returning('*'))[0]; +export const updateUserAnalytics = + (user_id: number, analytics: any) => async (tx: Knex.Transaction) => + ( + await tx('user_analytic') + .where({ user_id }) + .update(analytics) + .returning('*') + )[0]; + +export const getUserAnalytics = + (user_id: number) => async (tx: Knex.Transaction) => + (await tx('user_analytic').where({ user_id }))[0]; + export const updateProject = (id: string | number, project: Project) => async (tx: Knex.Transaction) => ( @@ -783,6 +800,7 @@ export const fetchAllBriefs = () => (tx: Knex.Transaction) => 'users.display_name as created_by', 'users.profile_photo as owner_photo', 'users.username as owner_name', + 'users.created as joined', 'experience_level', 'briefs.experience_id', 'briefs.created', @@ -1181,6 +1199,7 @@ export const fetchAllFreelancers = () => (tx: Knex.Transaction) => 'telegram_link', 'discord_link', 'title', + 'hour_per_rate', // 'bio', 'freelancers.user_id', 'username', @@ -1291,6 +1310,10 @@ export const fetchFreelancerClients = } }); +export const freelancerProjects = + (freelancer_id: number) => async (tx: Knex.Transaction) => + tx.select().where({ user_id: freelancer_id }).from('projects'); + export const fetchItems = (ids: number[], tableName: string) => async (tx: Knex.Transaction) => tx(tableName).select('id', 'name').whereIn(`id`, ids); @@ -1322,6 +1345,7 @@ export const insertFreelancerDetails = telegram_link: f.telegram_link, discord_link: f.discord_link, user_id: f.user_id, + hour_per_rate: f.hour_per_rate, }) .returning('id') @@ -1394,8 +1418,9 @@ export const updateFreelancerDetails = web3_type: string, web3_challenge: string, // eslint-disable-next-line unused-imports/no-unused-vars - freelancer_clients: Array<{ id: number; name: string; img: string }> + freelancer_clients: Array<{ id: number; name: string; img: string }>, // token: string + hour_per_rate: number ) => async (tx: Knex.Transaction) => await tx('freelancers') @@ -1412,6 +1437,7 @@ export const updateFreelancerDetails = telegram_link: f.telegram_link, discord_link: f.discord_link, user_id: f.user_id, + hour_per_rate, }) .where({ user_id: userId }) .returning('id') diff --git a/src/pages/api/analytics/getanalytics.ts b/src/pages/api/analytics/getanalytics.ts new file mode 100644 index 00000000..727b8672 --- /dev/null +++ b/src/pages/api/analytics/getanalytics.ts @@ -0,0 +1,85 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import nextConnect from 'next-connect'; +import passport from 'passport'; + +import { + getUserAnalytics, + insertUserAnalytics, + updateUserAnalytics, +} from '@/lib/models'; + +import db from '@/db'; + +export default nextConnect() + .use(passport.initialize()) + .post(async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { user_id } = req.body; + + const InitialDetails: any = { + user_id: user_id, + analytics: { + Monday: { + visitor: [], + count: 0, + date: 0, + }, + Tuesday: { + visitor: [], + count: 0, + }, + Wednesday: { + visitor: [], + count: 0, + }, + Thursday: { + visitor: [], + count: 0, + }, + Friday: { + visitor: [], + count: 0, + }, + Saturday: { + visitor: [], + count: 0, + }, + Sunday: { + visitor: [], + count: 0, + }, + }, + }; + db.transaction(async (tx) => { + const date = new Date().toLocaleDateString('en-US', { + weekday: 'long', + }); + const date1 = new Date(); + const userAnalytics = await getUserAnalytics(user_id)(tx); + if ( + userAnalytics && + date === 'Monday' && + userAnalytics.analytics['Monday'].date !== date1.getDate() + ) { + const newAnalytics = { + analytics: { + ...InitialDetails.analytics, + Monday: { + visitor: [], + count: 0, + date: 0, + }, + }, + }; + await updateUserAnalytics(user_id, newAnalytics)(tx); + return res.status(200).send(newAnalytics); + } + if (userAnalytics) return res.status(200).send(userAnalytics); + + const userAnalyticRes = await insertUserAnalytics(InitialDetails)(tx); + return res.status(200).send(userAnalyticRes); + }); + } catch (err) { + return res.send(err); + } + }); diff --git a/src/pages/api/analytics/index.ts b/src/pages/api/analytics/index.ts new file mode 100644 index 00000000..90fccd5c --- /dev/null +++ b/src/pages/api/analytics/index.ts @@ -0,0 +1,131 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import nextConnect from 'next-connect'; +import passport from 'passport'; + +import { + getUserAnalytics, + insertUserAnalytics, + updateUserAnalytics, +} from '@/lib/models'; + +import db from '@/db'; + +export default nextConnect() + .use(passport.initialize()) + .post(async (req: NextApiRequest, res: NextApiResponse) => { + try { + const { freelancer_id, user_id } = req.body; + const InitialDetails: any = { + user_id: freelancer_id, + analytics: { + Monday: { + visitor: [], + count: 0, + date: 0, + }, + Tuesday: { + visitor: [], + count: 0, + }, + Wednesday: { + visitor: [], + count: 0, + }, + Thursday: { + visitor: [], + count: 0, + }, + Friday: { + visitor: [], + count: 0, + }, + Saturday: { + visitor: [], + count: 0, + }, + Sunday: { + visitor: [], + count: 0, + }, + }, + }; + + db.transaction(async (tx) => { + const userAnalytics = await getUserAnalytics(freelancer_id)(tx); + const date1 = new Date(); + const date = new Date().toLocaleDateString('en-US', { + weekday: 'long', + }); + if ( + date == 'Monday' && + userAnalytics && + userAnalytics.analytics['Monday'].date !== date1.getDate() && + !userAnalytics.analytics['Monday'].visitor.includes(user_id) + ) { + const newCount = { + analytics: { + ...InitialDetails.analytics, + [date]: { + visitor: [...userAnalytics.analytics[date].visitor, user_id], + count: userAnalytics.analytics[date].count + 1, + date: date1.getDate(), + }, + }, + }; + const resp = await updateUserAnalytics(freelancer_id, newCount)(tx); + return res.status(200).json(resp); + } + if (date == 'Monday' && userAnalytics == undefined) { + const newCount = { + analytics: { + ...InitialDetails, + [date]: { + visitor: [...userAnalytics.analytics[date].visitor, user_id], + count: userAnalytics.analytics[date].count + 1, + date: date1.getDate(), + }, + }, + }; + const resp = await insertUserAnalytics(newCount)(tx); + return res.status(201).json(resp); + } + + if (userAnalytics == undefined) { + const userAnalyticsDetails = { + ...InitialDetails, + }; + userAnalyticsDetails.analytics[date] = { + visitor: [user_id], + count: 1, + }; + const userAnalyticsRes = await insertUserAnalytics( + userAnalyticsDetails + )(tx); + return res.status(200).json(userAnalyticsRes); + } + const isAlreadyVisited = + userAnalytics?.analytics[date].visitor.includes(user_id); + if (!isAlreadyVisited && userAnalytics) { + const newCount = { + analytics: { + ...userAnalytics.analytics, + [date]: { + ...userAnalytics.analytics[date], + visitor: [...userAnalytics.analytics[date].visitor, user_id], + count: userAnalytics.analytics[date].count + 1, + }, + }, + }; + const userAnalyticsRes = await updateUserAnalytics( + freelancer_id, + newCount + )(tx); + return res.status(200).json(userAnalyticsRes); + } + }); + + return res.status(201); + } catch (cause) { + return res.status(401).json(cause); + } + }); diff --git a/src/pages/api/briefs/index.ts b/src/pages/api/briefs/index.ts index 0d5842d8..0d2effb8 100644 --- a/src/pages/api/briefs/index.ts +++ b/src/pages/api/briefs/index.ts @@ -7,6 +7,8 @@ import passport from 'passport'; import { fetchAllBriefs, fetchItems, + fetchProjectById, + fetchUserBriefs, incrementUserBriefSubmissions, insertBrief, upsertItems, @@ -48,6 +50,20 @@ export default nextConnect() await Promise.all([ currentData, ...currentData.map(async (brief: any) => { + const userProject = await fetchUserBriefs(brief.user_id)(tx); + const user_hire_history = await Promise.all( + userProject.map(async (brief: any) => { + if (brief.project_id) { + const re = await fetchProjectById(brief.project_id)(tx); + return { + project_status: re?.status_id, + cost: re?.total_cost_without_fee, + }; + } + return { project_status: 0 }; + }) + ); + brief.user_hire_history = user_hire_history; brief.skills = await fetchItems(brief.skill_ids, 'skills')(tx); brief.industries = await fetchItems( brief.industry_ids, diff --git a/src/pages/api/freelancers/[id]/index.ts b/src/pages/api/freelancers/[id]/index.ts index ef1e401d..b13cf3ca 100644 --- a/src/pages/api/freelancers/[id]/index.ts +++ b/src/pages/api/freelancers/[id]/index.ts @@ -181,7 +181,7 @@ export default nextConnect() const web3_type = freelancer.web3_type; const web3_challenge = freelancer.web3_challenge; const freelancer_clients = freelancer?.clients; - + const hour_per_rate = freelancer.hour_per_rate; // const token = await models.generateGetStreamToken(freelancer); await models.updateGetStreamUserName({ ...userAuth, @@ -214,7 +214,8 @@ export default nextConnect() web3_address, web3_type, web3_challenge, - freelancer_clients + freelancer_clients, + hour_per_rate // token )(tx); diff --git a/src/pages/api/freelancers/index.ts b/src/pages/api/freelancers/index.ts index f958bc62..ef0e2b03 100644 --- a/src/pages/api/freelancers/index.ts +++ b/src/pages/api/freelancers/index.ts @@ -7,6 +7,7 @@ import { fetchAllFreelancers, fetchFreelancerClients, fetchFreelancerMetadata, + freelancerProjects, insertFreelancerDetails, upsertItems, } from '@/lib/models'; @@ -46,6 +47,9 @@ export default nextConnect() freelancer.clients = await fetchFreelancerClients( freelancer.id )(tx); + freelancer.projects = await freelancerProjects(freelancer.id)( + tx + ); }), ]); diff --git a/src/pages/api/project/index.ts b/src/pages/api/project/index.ts index 5e037a85..cdc4bbfe 100644 --- a/src/pages/api/project/index.ts +++ b/src/pages/api/project/index.ts @@ -72,7 +72,7 @@ export default nextConnect() total_cost_without_fee, imbue_fee, duration_id, - payment_address + payment_address, // project_type: project_type ?? models.ProjectType.Brief })(tx); diff --git a/src/pages/auth/sign-up/index.tsx b/src/pages/auth/sign-up/index.tsx index e0287236..960c72f4 100644 --- a/src/pages/auth/sign-up/index.tsx +++ b/src/pages/auth/sign-up/index.tsx @@ -3,40 +3,46 @@ import { SignerResult } from "@polkadot/api/types"; import { WalletAccount } from "@talismn/connect-wallets"; import bcrypt from 'bcryptjs'; // const PasswordStrengthBar = dynamic(() => import('react-password-strength-bar')); -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useRef, useState } from "react"; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { useRef, useState } from 'react'; // import PasswordStrengthBar from "react-password-strength-bar"; -const PasswordStrengthBar = dynamic(() => import('react-password-strength-bar'), { - ssr: false, -}) +const PasswordStrengthBar = dynamic( + () => import('react-password-strength-bar'), + { + ssr: false, + } +); -import dynamic from "next/dynamic"; +import dynamic from 'next/dynamic'; -import { matchedByUserName, matchedByUserNameEmail } from "@/utils"; -import { isUrlAndSpecialCharacterExist, isValidEmail, validateInputLength } from "@/utils/helper"; +import { matchedByUserName, matchedByUserNameEmail } from '@/utils'; +import { + isUrlAndSpecialCharacterExist, + isValidEmail, + validateInputLength, +} from '@/utils/helper'; -import Carousel from "@/components/Carousel/Carousel"; -import GoogleSignIn from "@/components/GoogleSignIn"; -import Web3WalletModal from "@/components/WalletModal/Web3WalletModal"; +import Carousel from '@/components/Carousel/Carousel'; +import GoogleSignIn from '@/components/GoogleSignIn'; +import Web3WalletModal from '@/components/WalletModal/Web3WalletModal'; import { postAPIHeaders } from '@/config'; -import { authorise, getAccountAndSign } from "@/redux/services/polkadotService"; - +import { authorise, getAccountAndSign } from '@/redux/services/polkadotService'; type FormErrorMessage = { - username: string - email: string - password: string - confirmPassword: string -} + username: string; + email: string; + password: string; + confirmPassword: string; +}; const initialState = { username: '', email: '', password: '', confirmPassword: '', -} +}; const invalidUsernames = [ 'username', 'imbue', @@ -65,27 +71,26 @@ export default function SignIn() { const [email, setEmail] = useState(); const [polkadotAccountsVisible, showPolkadotAccounts] = useState(false); const [showPassword, setShowPassword] = useState(false); - const [password, setPassword] = useState(""); + const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(initialState); - const router = useRouter() - + const router = useRouter(); const fullData = user?.length && email?.length && password?.length; - const ErrorFound = !!(error.confirmPassword?.length || + const ErrorFound = !!( + error.confirmPassword?.length || error.password?.length || error.username?.length || - error.email?.length) + error.email?.length + ); - const disableSubmit = - !fullData || - loading || ErrorFound + const disableSubmit = !fullData || loading || ErrorFound; const salt = bcrypt.genSaltSync(10); const redirect = (path: string) => { window.location.href = `${window.location.origin}/${path}`; - } + }; const handleSubmit = async (e: any) => { e.preventDefault(); @@ -106,7 +111,8 @@ export default function SignIn() { }); if (resp.ok) { - redirect("/dashboard"); + window.localStorage.setItem('newUser', '1'); + redirect('/dashboard'); } else { const errorMessage = await resp.json(); @@ -128,7 +134,7 @@ export default function SignIn() { account ); if (resp.ok) { - redirect("/dashboard"); + redirect('/dashboard'); } } catch (error) { // FIXME: error handling @@ -136,7 +142,6 @@ export default function SignIn() { } }; - const handleChange = async (event: any) => { const { name, value } = event.target; if (name === 'user') { @@ -145,56 +150,50 @@ export default function SignIn() { setError((val) => { return { ...val, - username: 'Username already taken' - } + username: 'Username already taken', + }; }); return; - } - else if (invalidUsernames.includes(value)) { - + } else if (invalidUsernames.includes(value)) { setError((val) => { return { ...val, - username: 'Username is not allowed' - } + username: 'Username is not allowed', + }; }); - } - else if (value.includes(" ")) { + } else if (value.includes(' ')) { setError((val) => { return { ...val, - username: 'Username cannot contain spaces' - } + username: 'Username cannot contain spaces', + }; }); return; - } - else if (!validateInputLength(value, 5, 30)) { + } else if (!validateInputLength(value, 5, 30)) { setError((val) => { return { ...val, - username: 'Username must be between 5 and 30 characters' - } + username: 'Username must be between 5 and 30 characters', + }; }); return; - } - else if (isUrlAndSpecialCharacterExist(value)) { + } else if (isUrlAndSpecialCharacterExist(value)) { setError((val) => { return { ...val, - username: 'Username cannot contain special characters or url' - } + username: 'Username cannot contain special characters or url', + }; }); return; - } - else { + } else { setError((val) => { return { ...val, - username: '' - } + username: '', + }; }); setUser(value); - }; + } } if (name === 'email') { const data = await matchedByUserNameEmail(value); @@ -203,26 +202,24 @@ export default function SignIn() { setError((val) => { return { ...val, - email: 'Email is invalid' - } + email: 'Email is invalid', + }; }); return; - } - else if (data) { + } else if (data) { setError((val) => { return { ...val, - email: 'Email already in use' - } + email: 'Email already in use', + }; }); return; - } - else { + } else { setError((val) => { return { ...val, - email: '' - } + email: '', + }; }); setEmail(value); } @@ -234,26 +231,25 @@ export default function SignIn() { setError((val) => { return { ...val, - password: 'password must be at least 5 characters' - } + password: 'password must be at least 5 characters', + }; }); return; - } - else if (!validatePassword(value)) { + } else if (!validatePassword(value)) { setError((val) => { return { ...val, - password: 'Password must be between 6 and 15 characters and contain at least one number and one special character' - } + password: + 'Password must be between 6 and 15 characters and contain at least one number and one special character', + }; }); - } - else { + } else { setError((val) => { return { ...val, - password: '' - } + password: '', + }; }); } setPassword(value); @@ -263,26 +259,27 @@ export default function SignIn() { } }; - const closeModal = (): void => { showPolkadotAccounts(true); }; - const walletRef = useRef(null) + const walletRef = useRef(null); return ( //
-
-
-
-
+
+
+
+
-
-

Sign up to Imbue Network

-

Make web3 work for you

-
+
+

+ Sign up to Imbue Network +

+

Make web3 work for you

+
  • -
    +
  • Wallet-icon -

    Sign up with wallet

    +

    + Sign up with wallet +

  • @@ -356,7 +358,10 @@ export default function SignIn() { autoComplete='off' placeholder='victordoe' onChange={handleChange} - onKeyDown={(e) => (e.key === 'Enter') && document.getElementsByName('password')[0].focus()} + onKeyDown={(e) => + e.key === 'Enter' && + document.getElementsByName('password')[0].focus() + } required className='outlinedInput text-[0.875rem] !border-[#BCBCBC] focus-within:outline focus-within:outline-1 focus-within:outline-imbue-purple evenShadow' name='user' @@ -375,23 +380,38 @@ export default function SignIn() { color='secondary' notched={false} className='h-[2.6rem] pl-[6px] text-[0.875rem]' - inputProps={ - { className: 'placeholder:text-[#D1D1D1] !text-black' } - } + inputProps={{ + className: 'placeholder:text-[#D1D1D1] !text-black', + }} placeholder='*********' type={showPassword ? 'text' : 'password'} name='password' onChange={handleChange} - onKeyDown={(e) => (e.key === 'Enter') && document.getElementsByName('submit')[0].click()} + onKeyDown={(e) => + e.key === 'Enter' && + document.getElementsByName('submit')[0].click() + } endAdornment={ setShowPassword(!showPassword)} edge='end' - className="mr-0" + className='mr-0' > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )} } @@ -418,45 +438,59 @@ export default function SignIn() {
    -

    +

    {error.password}

    -
    -

    Password strength requirement

    -
    -
    -

    8+

    -

    Characters

    +
    +

    Password strength requirement

    +
    +
    +

    + 8+ +

    +

    Characters

    -
    -

    AA

    -

    Uppercase

    +
    +

    + AA +

    +

    Uppercase

    -
    -

    aa

    -

    Lowercase

    +
    +

    + aa +

    +

    Lowercase

    -
    -

    123

    -

    Numbers

    +
    +

    + 123 +

    +

    Numbers

    -
    -

    $#^

    -

    Symbol

    +
    +

    + $#^ +

    +

    Symbol

    -
    - - Already on Imbue? - + Already on Imbue? { router.push("/auth/sign-in") }} + onClick={() => { + router.push('/auth/sign-in'); + }} > Sign In
    -

    By signing up, you agree with Imbue’s Terms & Conditions and Privacy Policy.

    +

    + By signing up, you agree with Imbue’s{' '} + + Terms & Conditions + {' '} + and Privacy Policy. +

    @@ -493,9 +537,9 @@ export default function SignIn() { {...{ polkadotAccountsVisible, showPolkadotAccounts, - accountSelected + accountSelected, }} />
    - ) -} \ No newline at end of file + ); +} diff --git a/src/pages/briefs/[id].tsx b/src/pages/briefs/[id].tsx index d612fc27..1e8cca92 100644 --- a/src/pages/briefs/[id].tsx +++ b/src/pages/briefs/[id].tsx @@ -177,14 +177,16 @@ const BriefDetails = (): JSX.Element => { return (

    Similar projects on Imbue

    setShowSimilarBrief(!showSimilarBrief)} diff --git a/src/pages/dashboard/ClientDashboard.tsx b/src/pages/dashboard/ClientDashboard.tsx index 9b5e8a4b..684c3e8f 100644 --- a/src/pages/dashboard/ClientDashboard.tsx +++ b/src/pages/dashboard/ClientDashboard.tsx @@ -131,7 +131,9 @@ export default function ClientDashboard() { const totalSpent = useMemo(() => { const total = Briefs?.acceptedBriefs?.reduce( (acc: number, item: any) => - acc + Number(item.project.total_cost_without_fee), + item.project.status_id === 6 + ? acc + Number(item.project.total_cost_without_fee) + : acc + 0, 0 ); return total; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 634b2405..a86d68d5 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -16,7 +16,10 @@ const LoginPopup = dynamic(() => import('@/components/LoginPopup/LoginPopup')); // import LoginPopup from '@/components/LoginPopup/LoginPopup'; +import { Modal } from '@mui/material'; + import { AppContext, AppContextType } from '@/components/Layout'; +import WelcomForNewUser from '@/components/WelcomeModalContent/WelcomeForNewUser'; import { Project, User } from '@/model'; import { Brief } from '@/model'; @@ -36,6 +39,7 @@ export type DashboardProps = { const Dashboard = (): JSX.Element => { const [loginModal, setLoginModal] = useState(false); const [client, setClient] = useState(); + const [newUser, setNewUser] = useState(false); const { user, loading: loadingUser, @@ -48,39 +52,51 @@ const Dashboard = (): JSX.Element => { const dispatch = useDispatch(); + useEffect(() => { + const entity = window.localStorage.getItem('newUser'); + if (entity) { + setNewUser(true); + window.localStorage.removeItem('newUser'); + } + }, []); - const setup = async () => { - try { - if (!user?.username && !loadingUser) return router.push('/'); - const client = await getStreamChat(); - setClient(client); - if(client && user.getstream_token) { - client?.connectUser( - { - id: String(user.id), - username: user.username, - name: user.display_name, - }, - user.getstream_token - ); - const result = await client.getUnreadCount(); - dispatch(setUnreadMessage({ message: result.channels.length })); - client.on((event) => { - if (event.total_unread_count !== undefined) { - dispatch(setUnreadMessage({ message: event.unread_channels })); - } - }); + useEffect(() => { + const setupStreamChat = async () => { + try { + if (!user?.username && !loadingUser) return router.push('/'); + setClient(await getStreamChat()); + } catch (error) { + setError({ message: error }); + } finally { + setLoadingStreamChat(false); } - setLoadingStreamChat(false); + }; - } catch (error) { - setError({ message: error }); - } - }; + setupStreamChat(); + }, [user]); useEffect(() => { - setup(); - }, [user]); + 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 })); + }; + getUnreadMessageChannels(); + client.on((event) => { + if (event.total_unread_count !== undefined) { + dispatch(setUnreadMessage({ message: event.unread_channels })); + } + }); + } + }, [client, user?.getstream_token, user?.username, loadingStreamChat]); const { profileView } = useContext(AppContext) as AppContextType; @@ -90,7 +106,9 @@ const Dashboard = (): JSX.Element => { return client ? (
    - + + + { const [client, setClient] = useState(); const { @@ -47,9 +50,10 @@ const FreelancerDashboard = (): JSX.Element => { const [showMessageBox, setShowMessageBox] = useState(false); const [targetUser, setTargetUser] = useState(null); const [loadingStreamChat, setLoadingStreamChat] = useState(true); - + const [userAnalytics, setUserAnalytics] = useState([]); + const [totalViews, setTotalViews] = useState(0); const [messageList, setMessageList] = useState< - FormatMessageResponse[] | null + Channel[] | null >(); const router = useRouter(); @@ -105,6 +109,34 @@ const FreelancerDashboard = (): JSX.Element => { setupStreamChat(); }, [loadingUser, router, user?.id, user?.username]); + useEffect(() => { + const getUserAnalyt = async () => { + const resp = await getUserAnalytics(user.id); + const key = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]; + let totalViews = 0; + const data = key.map((item: string) => { + totalViews += resp?.analytics[item]?.count; + return { + label: item, + value: resp?.analytics[item]?.count, + }; + }); + setTotalViews(totalViews); + setUserAnalytics(data); + }; + if (user && user.id) { + getUserAnalyt(); + } + }, [user]); + useEffect(() => { if (client && user?.username && !loadingStreamChat) { client?.connectUser( @@ -131,9 +163,9 @@ const FreelancerDashboard = (): JSX.Element => { watch: true, // this is the default state: true, }); - const lastMessages: FormatMessageResponse[] = []; + const lastMessages: Channel[] = []; channels.map((channel) => { - lastMessages.push(channel.lastMessage()); + lastMessages.push(channel); }); setMessageList(lastMessages); }; @@ -370,22 +402,19 @@ const FreelancerDashboard = (): JSX.Element => { currentUser: user, }} /> -
    +
    {/* Starting of graph */} - {/*
    +

    Analytics

    - +

    {totalViews} views

    -

    - 124 -

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

    Messaging

    diff --git a/src/pages/freelancers/[slug].tsx b/src/pages/freelancers/[slug].tsx index 42886691..c6b314c4 100644 --- a/src/pages/freelancers/[slug].tsx +++ b/src/pages/freelancers/[slug].tsx @@ -10,6 +10,7 @@ import { InputAdornment, InputLabel, MenuItem, + OutlinedInput, Select, TextField, Tooltip, @@ -64,6 +65,7 @@ import SuccessScreen from '@/components/SuccessScreen'; import { Currency, Freelancer, Project, User } from '@/model'; import { fetchUserRedux } from '@/redux/reducers/userReducers'; +import { setUserAnalytics } from '@/redux/services/briefService'; import { getFreelancerApplications, getFreelancerProfile, @@ -101,6 +103,9 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { const memberSince = moment(freelancer?.created).format('MMMM YYYY'); const [prevUserName, setprevUserName] = useState(freelancer.username); const [titleError, settitleError] = useState(null); + const [hourperrate, setHourPerrate] = useState( + freelancer.hour_per_rate + ); const [skills, setSkills] = useState( freelancer?.skills?.map( (skill: { id: number; name: string }) => @@ -108,7 +113,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { ) ); - const { user: browsingUser } = useSelector( + const { user: browsingUser, loading: browsingUserLoading } = useSelector( (state: RootState) => state.userState ); const dispatch = useDispatch(); @@ -196,6 +201,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { skills: skills, clients: clients, logged_in_user: browsingUser.id === initFreelancer.user_id, + hour_per_rate: hourperrate, }; const resp: any = await updateFreelancer(data); @@ -203,7 +209,9 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { setSuccess(true); setprevUserName(data.username); } else { - setError({ message: 'Someting went wrong' + JSON.stringify(resp.message) }); + setError({ + message: 'Someting went wrong' + JSON.stringify(resp.message), + }); } } } catch (error) { @@ -213,11 +221,19 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { } }; + useEffect(() => { + const updateFreelancerAnalytics = async () => { + await setUserAnalytics(browsingUser.id, freelancer.user_id); + }; + if (!browsingUserLoading && browsingUser.id !== freelancer.user_id) { + updateFreelancerAnalytics(); + } + }, [browsingUser]); + const handleMessageBoxClick = () => { if (browsingUser.id) { setShowMessageBox(true); - } - else { + } else { setShowLoginPopup(true); } }; @@ -238,8 +254,13 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { else settitleError(null); } if (e.target.name === 'display_name') { - if (newFreelancer.display_name.trim().length < 1 && newFreelancer.display_name.trim().length > 30) { - setDisplayNameError('Display name must be between 1 to 30 characters long'); + if ( + newFreelancer.display_name.trim().length < 1 && + newFreelancer.display_name.trim().length > 30 + ) { + setDisplayNameError( + 'Display name must be between 1 to 30 characters long' + ); } else if (isUrlExist(e.target.value)) { setDisplayNameError( 'URL and special characters are not allowed in display name' @@ -499,7 +520,8 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { ...initFreelancer, skills: freelancer?.skills?.map( (skill: { id: number; name: string }) => - skill?.name?.charAt(0).toUpperCase() + skill?.name?.slice(1) + skill?.name?.charAt(0).toUpperCase() + + skill?.name?.slice(1) ), logged_in_user: browsingUser, }} @@ -518,7 +540,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { label='Name' variant='outlined' color='secondary' - defaultValue={freelancer?.display_name || ""} + defaultValue={freelancer?.display_name || ''} autoComplete='off' inputProps={{ maxLength: 30 }} /> @@ -530,20 +552,60 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { {displayError}

    )} + + + + payment per hour + + + setHourPerrate( + event.target.value > 0 + ? Math.trunc(event.target.value) + : undefined + ) + } + className='w-full' + value={hourperrate} + defaultValue={freelancer?.hour_per_rate} + placeholder='0.00' + type='number' + color='secondary' + startAdornment={ + + $ + + } + /> + ) : ( -
    -

    - {freelancer?.display_name} -

    - {initFreelancer?.verified && ( -
    - - - verified - -
    - )} +
    +
    +

    + {freelancer?.display_name} +

    + {initFreelancer?.verified && ( +
    + + + verified + +
    + )} +
    +

    + ${Number(freelancer?.hour_per_rate).toFixed(2)} + /hr +

    )} @@ -558,7 +620,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { label='Username' variant='outlined' color='secondary' - defaultValue={freelancer?.username || ""} + defaultValue={freelancer?.username || ''} autoComplete='off' inputProps={{ maxLength: 30 }} /> @@ -596,7 +658,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { name='title' label='Tittle' variant='outlined' - defaultValue={freelancer?.title || ""} + defaultValue={freelancer?.title || ''} autoComplete='off' /> {titleError && ( @@ -866,7 +928,9 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { { if (freelancer) { setFreelancer({ @@ -1030,7 +1094,7 @@ const Profile = ({ initFreelancer }: ProfileProps): JSX.Element => { <>