dAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJw b z_^v8bbg` SAn{I*4bH$u(RZ6*x UhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=p C^ S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk( $?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU ^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvh CL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c 70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397* _cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111a H}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*I cmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU &68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-= A= yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v #ix45EVrcEhr>!NMhprl $InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~ &^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7< 4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}sc Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+ 9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2 `1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M =hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S( O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/app/forgot-password/layout.tsx b/app/forgot-password/layout.tsx new file mode 100644 index 0000000..4cbd743 --- /dev/null +++ b/app/forgot-password/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +interface IProps { + children: ReactNode; +} + +export default function ForgotPasswordLayout({ children }: IProps) { + return children; +} diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx new file mode 100644 index 0000000..593640e --- /dev/null +++ b/app/forgot-password/page.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useState } from "react"; +import Image from "next/image"; +import LeftSection from "@/components/layout/AuthLayout/LeftSection"; +import { Typography } from "@/libraries/material-tailwind"; +import ForgotPasswordForm from "@/components/forgot-password/ForgotPasswordForm"; +import Loading from "@/components/custom/Loading"; + +export default function ForgotPasswordPage() { + const [loading, setLoading] = useState (false); + return ( + <> + + ++++ + Dental Jobs + ++++ + + Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida . + ++ {loading ? ( + + > + ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..3c37cf5 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,125 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +/* width */ +::-webkit-scrollbar { + width: 7px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: none; +} + +::-webkit-scrollbar-track:hover { + background: none; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + border-radius: 16px; + border: 5px solid #8032ff; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + border: 5px solid #6627cc; +} + +/* set button(top and bottom of the scrollbar) */ +::-webkit-scrollbar-button { + display: none; +} + +.react-calendar__month-view__weekdays abbr { + text-decoration: none; +} + +.react-calendar__tile { + border-radius: 9999px; +} + +.react-calendar__tile { + @media (max-width: 500px) { + padding: 10px 3px; + } +} + +.react-calendar__tile--now { + background: none !important; + border: 2px solid #8032ff !important; + color: #8032ff !important; + font-weight: 700 !important; +} + +.react-calendar__month-view__days__day { + color: #7a6899; +} + +.rbc-header { + padding: 0 !important; + border: none !important; +} + +.rbc-month-view { + border: none !important; +} + +.rbc-day-bg { + border: 1px solid #f6f4f9; + border-radius: 0.5rem; +} + +.rbc-month-row + .rbc-month-row { + border: none !important; +} + +.rbc-date-cell { + padding-top: 7px !important; + padding-right: 7px !important; + color: #7a6899 !important; +} + +.rbc-event { + background-color: transparent !important; +} + +.rbc-date-cell.rbc-now.rbc-current { + color: #8032ff !important; +} + +.tooltip-center-small-screen { + @media (max-width: 500px) { + position: fixed; + top: 10%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + width: 82%; + } +} diff --git a/app/help/layout.tsx b/app/help/layout.tsx new file mode 100644 index 0000000..5dca850 --- /dev/null +++ b/app/help/layout.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from "react"; + +export default function DashboardLayout({ children }: { children: ReactNode }) { + return+ ) : ( + ++ )} ++ Forgot Password? + ++ {children} ; +} diff --git a/app/help/page.tsx b/app/help/page.tsx new file mode 100644 index 0000000..6b42f8a --- /dev/null +++ b/app/help/page.tsx @@ -0,0 +1,18 @@ +"use client" + +import HelpPage from "@/components/help"; +import { useUser } from "@/contexts/UserContext"; +import { useEffect } from "react"; + +export default function Dashboard() { + const {userData} = useUser(); + + useEffect(() => { + if(!userData || userData?.userType == 1) return ; + }, []) + return ( + <> ++ > + ); +} diff --git a/app/job-posting/layout.tsx b/app/job-posting/layout.tsx new file mode 100644 index 0000000..ef37db8 --- /dev/null +++ b/app/job-posting/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +export default function JobPostingLayout({ + children, +}: { + children: ReactNode; +}) { + return {children} ; +} diff --git a/app/job-posting/page.tsx b/app/job-posting/page.tsx new file mode 100644 index 0000000..e4920e5 --- /dev/null +++ b/app/job-posting/page.tsx @@ -0,0 +1,9 @@ +import JobPostingPage from "@/components/job-posting/JobPostingPage"; + +export default function Booking() { + return ( + <> ++ > + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..35c76ee --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,46 @@ +/* eslint-disable @next/next/no-page-custom-font */ +import type { Metadata } from "next"; +import { ThemeProvider } from "@/libraries/material-tailwind"; +import "react-circular-progressbar/dist/styles.css"; +import "react-calendar/dist/Calendar.css"; +import "react-big-calendar/lib/css/react-big-calendar.css"; +import "react-toastify/dist/ReactToastify.css"; +import "swiper/css"; +import "react-phone-number-input/style.css"; +import "./globals.css"; +import Home from "./page"; +import { ToastContainer } from "@/libraries/react-toastify"; + +export const metadata: Metadata = { + title: "Dental", + description: "Dental Job Portal", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + + + + + +{children} ++ + + ); +} diff --git a/app/messages/layout.tsx b/app/messages/layout.tsx new file mode 100644 index 0000000..429dd00 --- /dev/null +++ b/app/messages/layout.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from "react"; + +export default function MessagesLayout({ children }: { children: ReactNode }) { + return <>{children}>; +} diff --git a/app/messages/page.tsx b/app/messages/page.tsx new file mode 100644 index 0000000..241518e --- /dev/null +++ b/app/messages/page.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { TEMP_USERS } from "@/components/messages/tempData"; +import { IUserItem } from "@/utils/interfaces"; +import DP from "@/components/messages/DP"; +import MB from "@/components/messages/MB"; +import { useUser } from "@/contexts/UserContext"; + +export type TUserItem = IUserItem | null; + +export default function MessagesPage() { + const [selectedUser, setSelectedUser] = useState (null); + const {userData} = useUser(); + + useEffect(() => { + if(!userData || userData?.userType == 1) return ; + }, []) + + return ( + <> + + + > + ); +} diff --git a/app/otp-verify/layout.tsx b/app/otp-verify/layout.tsx new file mode 100644 index 0000000..e8061ad --- /dev/null +++ b/app/otp-verify/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +interface IProps { + children: ReactNode; +} + +export default function OTPVerifyLayout({ children }: IProps) { + return children; +} diff --git a/app/otp-verify/page.tsx b/app/otp-verify/page.tsx new file mode 100644 index 0000000..f25431c --- /dev/null +++ b/app/otp-verify/page.tsx @@ -0,0 +1,74 @@ +"use client"; +import Image from "next/image"; +import LeftSection from "@/components/layout/AuthLayout/LeftSection"; +import OTPVerifyForm from "@/components/otp-verify/OTPVerifyForm"; +import { Typography } from "@/libraries/material-tailwind"; +import { useState } from "react"; +import Loading from "@/components/custom/Loading"; + +export default function OTPVerifyPage() { + const [loading, setLoading] = useState (false); + + return ( + <> + + + ++++ + Dental Jobs + ++++ + + Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida . + ++ {loading ? ( + + > + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..8e6e76e --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,37 @@ +"use client"; +import { ReactNode } from "react"; +import { usePathname } from "next/navigation"; +import { LoadingProvider } from "@/contexts/LoadingContext"; +import { CalendarProvider } from "@/contexts/CalendarContext"; +import { ScheduleProvider } from "@/contexts/ScheduleContext"; +import { isAuthLayout } from "@/utils/functions"; +import AuthLayout from "@/components/layout/AuthLayout"; +import DashboardLayout from "@/components/layout/DashboardLayout"; +import { UserProvider } from "@/contexts/UserContext"; +import { AuthProvider } from "@/contexts/AuthContext"; + +interface IProps { + children: ReactNode; +} + +export default function Home({ children }: IProps) { + const pathname = usePathname(); + + return ( ++ ) : ( + ++ )} ++ OTP Verification + ++ + + ); +} diff --git a/app/revenue/layout.tsx b/app/revenue/layout.tsx new file mode 100644 index 0000000..2f47813 --- /dev/null +++ b/app/revenue/layout.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from "react"; + +export default function RevenueLayout({ children }: { children: ReactNode }) { + return+ ++ ++ ++ {isAuthLayout(pathname) ? ( + +{children} + ) : ( +{children} + )} +{children} ; +} diff --git a/app/revenue/page.tsx b/app/revenue/page.tsx new file mode 100644 index 0000000..66c7e5d --- /dev/null +++ b/app/revenue/page.tsx @@ -0,0 +1,18 @@ +"use client" +import RevenuePage from "@/components/revenue"; +import { useUser } from "@/contexts/UserContext"; +import { useEffect } from "react"; + +export default function Revenue() { + const {userData} = useUser(); + + useEffect(() => { + if(!userData || userData?.userType == 1) return ; + }, []) + + return ( + <> ++ > + ); +} diff --git a/app/schedule/layout.tsx b/app/schedule/layout.tsx new file mode 100644 index 0000000..93264e5 --- /dev/null +++ b/app/schedule/layout.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from "react"; + +export default function ScheduleLayout({ children }: { children: ReactNode }) { + return ( + {children} + ); +} diff --git a/app/schedule/page.tsx b/app/schedule/page.tsx new file mode 100644 index 0000000..61ceeea --- /dev/null +++ b/app/schedule/page.tsx @@ -0,0 +1,46 @@ +"use client"; +import { useEffect } from "react"; +import { toast } from "@/libraries/react-toastify"; +import api from "@/utils/api"; +import { getErrorMessage } from "@/utils/functions"; +import { useSchedule } from "@/contexts/ScheduleContext"; +import Schedule from "@/components/schedule"; +import useLoading from "@/hooks/useLoading"; +import { useUser } from "@/contexts/UserContext"; + +let isFirstLoad = true; + +export default function SchedulePage() { + const { setWeekAvailabilities } = useSchedule(); + const { setIsLoading } = useLoading(); + const { userData } = useUser(); + + useEffect(() => { + if (!userData || userData?.userType == 1) return; + if (isFirstLoad) { + (async () => { + setIsLoading(true); + await api + .get("/schedule/get/availability") + .then((res) => { + const { data } = res.data; + setWeekAvailabilities(data?.daysAvailable || []); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + + await api.get("/schedule/get/blocked/dates").then((res) => { + const { data } = res.data; + if (data) { + } + }); + setIsLoading(false); + })(); + } + isFirstLoad = false; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return; +} diff --git a/app/signin/layout.tsx b/app/signin/layout.tsx new file mode 100644 index 0000000..f3f572d --- /dev/null +++ b/app/signin/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +interface IProps { + children: ReactNode; +} + +export default function SigninLayout({ children }: IProps) { + return <>{children}>; +} diff --git a/app/signin/page.tsx b/app/signin/page.tsx new file mode 100644 index 0000000..ccbf1f0 --- /dev/null +++ b/app/signin/page.tsx @@ -0,0 +1,107 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import LeftSection from "@/components/layout/AuthLayout/LeftSection"; +import { Typography } from "@/libraries/material-tailwind"; +import SigninForm from "@/components/signin/SigninForm"; +import { PATH_MAPPER } from "@/utils/constants"; +import { useState } from "react"; +import Loading from "@/components/custom/Loading"; + +export default function SigninPage() { + const [loading, setLoading] = useState (false); + return ( + <> + + + ++++ + Dental Jobs + ++++ + + Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida . + ++ {loading ? ( + + > + ); +} diff --git a/app/signup/layout.tsx b/app/signup/layout.tsx new file mode 100644 index 0000000..aaa5681 --- /dev/null +++ b/app/signup/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +interface IProps { + children: ReactNode; +} + +export default function SignupLayout({ children }: IProps) { + return <>{children}>; +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 0000000..1a51665 --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,121 @@ +"use client"; +import Image from "next/image"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { useEffect, useState } from "react"; +import LeftSection from "@/components/layout/AuthLayout/LeftSection"; +import Signup from "@/components/signup/Signup"; +import { Typography } from "@/libraries/material-tailwind"; +import { L_STORAGE_AUTH_TOKEN } from "@/utils/constants"; + +export default function SignupPage() { + const [authToken, setAuthToken] = useState+ ) : ( + <> + +++ + ++ > + )} ++ Login + ++ + Haven't created your free account? + +
+ + Sign up Dental Office + {" "} + or{" "} + + Dental Professional + +(""); + + useEffect(() => { + setAuthToken( + localStorage.getItem(L_STORAGE_AUTH_TOKEN) || + sessionStorage.getItem(L_STORAGE_AUTH_TOKEN) || + "" + ); + }, []); + + if (authToken) return redirect("/dashboard"); + + return ( + <> + + + ++++ + +++ + Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida . + ++ + + > + ); +} diff --git a/components/bookings/DTBooking.tsx b/components/bookings/DTBooking.tsx new file mode 100644 index 0000000..93fa170 --- /dev/null +++ b/components/bookings/DTBooking.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React from "react"; +import { Typography, ListItem } from "@/libraries/material-tailwind"; +import { TEMP_BOOKING } from "@/utils/tempData"; +import { IconButton } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import StatusBadge from "../custom/StatusBadge"; +import Link from "next/link"; + +interface IProps { + filterBookings: Function; +} + +export default function DTBooking({ filterBookings }: IProps) { + return ( ++++ + +++ Professional Registration + +++ ++ If you are a dental practice looking to hire, click{" "} + + here + {" "} + to register. + + ++ Please enter your full name and contact information. We will never + sell your information and it will only be used as part of our + service. In order to approve your profile, we will need to verify + your contact information. We may also contact you to help you + complete your application. If you would like to set up a meeting + now, please{" "} + + Schedule a time + {" "} + to speak with us. + ++ ++ ); +} diff --git a/components/bookings/MBBooking.tsx b/components/bookings/MBBooking.tsx new file mode 100644 index 0000000..8591f27 --- /dev/null +++ b/components/bookings/MBBooking.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React from "react"; +import { Typography, ListItem } from "@/libraries/material-tailwind"; +import { IconButton } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import StatusBadge from "../custom/StatusBadge"; +import Link from "next/link"; + +interface IProps { + filterBookings: Function; +} + +export default function MBBooking({ filterBookings }: IProps) { + return ( + <> + {filterBookings().map((b: any) => ( ++++ Date & Time + ++ Booking with + ++ Hourly Rate + ++ Message + ++ {filterBookings().map((b: any, i: number) => ( +++ + ))} ++ {b.bookedDateTime.slice(0, 10)} {b.bookedDateTime.slice(11, 19)} + ++ {b.bookingWith} + ++ {b.hourlyRate} + + + ++ + + + {/*+ {b.status} */} ++ + ))} + > + ); +} diff --git a/components/bookings/index.tsx b/components/bookings/index.tsx new file mode 100644 index 0000000..8a1b627 --- /dev/null +++ b/components/bookings/index.tsx @@ -0,0 +1,168 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { IComponent, IBooking } from "@/utils/interfaces"; +import CardTemplate from "../custom/CardTemplate"; +import TabButton from "../custom/buttons/TabButton"; +import { IconButton, Tooltip } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { TEMP_BOOKING } from "@/utils/tempData"; +import CalendarCard from "../dashboard/CalendarCard"; +import DTBooking from "./DTBooking"; +import MBBooking from "./MBBooking"; +import api from "@/utils/api"; +import { useUser } from "@/contexts/UserContext"; + +export default function BookingCard({ className = "" }: IComponent) { + const [activeTab, setActiveTab] = useState+++++ {b.label} + + ++ + + {/*+ {b.status} */} ++++ Date and Time + ++ {b.bookedDateTime.slice(0, 10)}-{b.bookedDateTime.slice(11, 17)} + ++++ Hourly Rate + ++ {b.hourlyRate} + +("Today"); + const [data, setData] = useState ([]); + const [isTooltipOpen, setIsTooltipOpen] = useState (false); + const [isSmallScreen, setIsSmallScreen] = useState (false); + const tooltipRef = useRef (null); + const iconButtonRef = useRef (null); + const calendarRef = useRef (null); + const pageNo: number = 1; + const { userData } = useUser(); + + const toggleTooltip = () => { + setIsTooltipOpen(!isTooltipOpen); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + (!tooltipRef.current || + !tooltipRef.current.contains(event.target as Node)) && + (!iconButtonRef.current || + !iconButtonRef.current.contains(event.target as Node)) && + (!calendarRef.current || + !calendarRef.current.contains(event.target as Node)) + ) { + setIsTooltipOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + useEffect(() => { + const checkScreenSize = () => { + setIsSmallScreen(window.innerWidth < 640); + }; + checkScreenSize(); + window.addEventListener("resize", checkScreenSize); + return () => window.removeEventListener("resize", checkScreenSize); + }, []); + + const getData = async () => { + let res: any = await api.post(`/jobs/professional/bookings/${pageNo}`); + setData(res?.data); + }; + + useEffect(() => { + if (!userData || userData?.userType == 1) return; + getData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function filterBookings() { + const currentDate = new Date().toDateString(); + switch (activeTab) { + case "Today": + return data.filter((booking) => { + const bookedDate = new Date(booking.bookedDateTime).toDateString(); + return bookedDate === currentDate; + }); + case "Past": + return data.filter((booking) => { + const bookedDate = new Date(booking.bookedDateTime).toDateString(); + return bookedDate < currentDate; + }); + case "Future": + return data.filter((booking) => { + const bookedDate = new Date(booking.bookedDateTime).toDateString(); + return bookedDate >= currentDate; + }); + default: + return data; + } + } + + return ( + + + ); +} diff --git a/components/create-password/CreatePasswordForm.tsx b/components/create-password/CreatePasswordForm.tsx new file mode 100644 index 0000000..eba7867 --- /dev/null +++ b/components/create-password/CreatePasswordForm.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { FormHTMLAttributes, useState } from "react"; +import * as yup from "yup"; +import Input from "@/components/custom/Input"; +import { IconButton } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { + ICON_MAPPER, + VALIDATION_DISMATCH_PASSWORDS, + VALIDATION_REQUIRED_FIELD, +} from "@/utils/constants"; +import { useFormik } from "formik"; +import Button from "@/components/custom/buttons/Button"; +import api from "@/utils/api"; +import { useAuth } from "@/contexts/AuthContext"; +import { toast } from "react-toastify"; +import { useRouter } from "next/navigation"; +import { getErrorMessage } from "@/utils/functions"; + +type TPasswordType = "text" | "password"; + +const validationSchema = yup.object().shape({ + password: yup.string().required(VALIDATION_REQUIRED_FIELD), + confPassword: yup + .string() + .oneOf([yup.ref("password", undefined)], VALIDATION_DISMATCH_PASSWORDS) + .required(VALIDATION_REQUIRED_FIELD), +}); + +interface IProps extends FormHTMLAttributes+ + + } + > ++ + ) : null + } + className={`bg-white py-3 px-4 ${ + isSmallScreen ? "tooltip-center-small-screen" : "" + }`} + animate={{ + mount: { scale: 1, y: 0 }, + unmount: { scale: 0, y: 25 }, + }} + open={isTooltipOpen} + dismiss={{ + itemPress: false, + }} + > + + ++ ++++setActiveTab("Today")} + > + Today + +setActiveTab("Past")} + > + Past + +setActiveTab("Future")} + > + Future + ++++ +++ { + setLoading: (value: boolean | ((prev: boolean) => boolean)) => void; +} + +export default function CreatePasswordForm({ + className = "", + setLoading, +}: IProps) { + const router = useRouter(); + const { refId } = useAuth(); + + const [passwordType, setPasswordType] = useState ("password"); + const [confPasswordType, setConfPasswordType] = + useState ("password"); + + const formik = useFormik({ + initialValues: { + password: "", + confPassword: "", + }, + validationSchema, + onSubmit: ({ password }) => { + setLoading(true); + + api + .post("/update/forgot/password", { + password, + refId, + }) + .then((res) => { + const { success } = res.data; + setLoading(false); + if (success) { + toast.success("The password has been updated."); + router.push("/signin"); + } + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }, + }); + + return ( + + ); +} diff --git a/components/custom/CardTemplate.tsx b/components/custom/CardTemplate.tsx new file mode 100644 index 0000000..e9ed658 --- /dev/null +++ b/components/custom/CardTemplate.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from "react"; +import { IComponent } from "@/utils/interfaces"; +import { Card, CardBody, Typography } from "@/libraries/material-tailwind"; + +interface IProps extends IComponent { + title: string; + children: ReactNode; + actions?: ReactNode; +} + +export default function CardTemplate({ + className = "", + title = "", + children, + actions, +}: IProps) { + return ( + + + ); +} diff --git a/components/custom/Checkbox.tsx b/components/custom/Checkbox.tsx new file mode 100644 index 0000000..c6b0a5f --- /dev/null +++ b/components/custom/Checkbox.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { + CheckboxProps, + Checkbox as MTCheckbox, + Typography, +} from "@/libraries/material-tailwind"; +import { IComponent } from "@/utils/interfaces"; +import { TColor } from "@/utils/types"; +import { useEffect, useState } from "react"; + +interface IProps extends IComponent { + color: TColor; + label: CheckboxProps["label"]; + labelProps?: CheckboxProps["labelProps"]; + name?: CheckboxProps["name"]; + checked?: CheckboxProps["checked"]; + onChange?: CheckboxProps["onChange"]; +} + +export default function Checkbox({ + className: propClassName = "", + color = "primary", + label, + name, + checked, + onChange, +}: IProps) { + const [classNameOfIconProps, setClassNameOfIconProps] = + useState+ +++ {children} ++ {title} + + {title === "Hi, James Mann!" ? ( ++ Public Profile + + ) : ( + "" + )} + {actions} +("bg-primary"); + const [className, setClassName] = useState ( + "border-primary checked:border-primary checked:bg-primary", + ); + + useEffect(() => { + switch (color) { + case "secondary": + setClassNameOfIconProps("bg-secondary"); + return setClassName( + "border-secondary checked:border-secondary checked:bg-secondary", + ); + + case "success": + setClassNameOfIconProps("bg-success"); + return setClassName( + "border-success checked:border-success checked:bg-success", + ); + + case "info": + setClassNameOfIconProps("bg-info"); + return setClassName("border-info checked:border-info checked:bg-info"); + + case "warning": + setClassNameOfIconProps("bg-warning"); + return setClassName( + "border-warning checked:border-warning checked:bg-warning", + ); + + case "error": + setClassNameOfIconProps("bg-error"); + return setClassName( + "border-error checked:border-error checked:bg-error", + ); + + case "dark": + setClassNameOfIconProps("bg-dark"); + return setClassName("border-dark checked:border-dark checked:bg-dark"); + + case "lightDark": + setClassNameOfIconProps("bg-lightDark"); + return setClassName( + "border-lightDark checked:border-lightDark checked:bg-lightDark", + ); + + default: + setClassNameOfIconProps("bg-primary"); + return setClassName( + "border-primary checked:border-primary checked:bg-primary", + ); + } + }, [color]); + + return ( + + {label} + + } + labelProps={{ + className: "flex-1", + }} + name={name} + checked={checked} + onChange={onChange} + /> + ); +} diff --git a/components/custom/EmptyAvatar.tsx b/components/custom/EmptyAvatar.tsx new file mode 100644 index 0000000..66c3ec9 --- /dev/null +++ b/components/custom/EmptyAvatar.tsx @@ -0,0 +1,17 @@ +import { AllHTMLAttributes } from "react"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; + +export default function EmptyAvatar({ + className = "", + onClick, +}: AllHTMLAttributes ) { + return ( + ++ ); +} diff --git a/components/custom/Input.tsx b/components/custom/Input.tsx new file mode 100644 index 0000000..4242825 --- /dev/null +++ b/components/custom/Input.tsx @@ -0,0 +1,103 @@ +import { type ReactNode, type InputHTMLAttributes, useState } from "react"; +import { Typography } from "@/libraries/material-tailwind"; + +interface IProps extends InputHTMLAttributes+ { + label?: string; + id?: string; + type?: string; + name?: string; + className?: string; + children?: ReactNode | string; + classNameOfInput?: string; + startAdornment?: ReactNode; + endAdornment?: ReactNode; + error?: string | boolean; +} + +export default function Input({ + label = "", + id = "", + type = "", + name = "", + className = "", + classNameOfInput = "", + startAdornment, + endAdornment, + error, + ...others +}: IProps) { + const [showPassword, setShowPassword] = useState (false); + const [focused, setFocused] = useState (false); + return ( + + {!!label && ( + + )} + ++ ); +} diff --git a/components/custom/Loading.tsx b/components/custom/Loading.tsx new file mode 100644 index 0000000..a9004f8 --- /dev/null +++ b/components/custom/Loading.tsx @@ -0,0 +1,9 @@ +import { BarLoader } from "@/libraries/react-spinners"; + +export default function Loading() { + return ( ++ {startAdornment ? ( ++ + {!!error && ( ++ {startAdornment} + setFocused(true)} + onBlur={() => setFocused(false)} + /> ++ ) : ( + setFocused(true)} + onBlur={() => setFocused(false)} + /> + )} + + {!!endAdornment && ( +setShowPassword(true)} + onMouseUp={() => setShowPassword(false)} + > + {endAdornment} ++ )} ++ {error} + + )} +++ ); +} diff --git a/components/custom/MonthPick.tsx b/components/custom/MonthPick.tsx new file mode 100644 index 0000000..2d671d5 --- /dev/null +++ b/components/custom/MonthPick.tsx @@ -0,0 +1,51 @@ +import { AllHTMLAttributes, useMemo } from "react"; +import { IconButton, Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; + +interface IProps extends AllHTMLAttributes+ { + pickPrevMonth: () => void; + pickNextMonth: () => void; + date: Date; +} + +export default function MonthPick({ + pickPrevMonth, + pickNextMonth, + date, + className = "", +}: IProps) { + const monthYear = useMemo(() => { + const formatter = new Intl.DateTimeFormat("en-us", { + year: "numeric", + month: "long", + }); + return formatter.format(date).replaceAll(".", ""); + }, [date]); + + return ( + ++ ); +} diff --git a/components/custom/Select.tsx b/components/custom/Select.tsx new file mode 100644 index 0000000..a606ec9 --- /dev/null +++ b/components/custom/Select.tsx @@ -0,0 +1,59 @@ +import { ReactNode, SelectHTMLAttributes, useState } from "react"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; + +interface IProps extends SelectHTMLAttributes+ ++ + {monthYear} + ++ ++ { + children: ReactNode; + available?: boolean; + iconClassName?: string; + label?: string; + error?: string | boolean; +} + +export default function Select({ + className = "", + available, + children, + iconClassName = "", + label, + error, + id, + ...others +}: IProps) { + const [opened, setOpened] = useState (false); + + return ( + + {!!label && ( + + )} ++ ); +} diff --git a/components/custom/StatusBadge.tsx b/components/custom/StatusBadge.tsx new file mode 100644 index 0000000..db33924 --- /dev/null +++ b/components/custom/StatusBadge.tsx @@ -0,0 +1,57 @@ +import { ReactNode, useMemo } from "react"; +import { STATUS_MAPPER } from "@/utils/constants"; +import { IComponent } from "@/utils/interfaces"; + +interface IProps extends IComponent { + status: string; + isDefault?: boolean; + beta?: string; + children: ReactNode; +} + +export default function StatusBadge({ + className = "", + status = "", + isDefault = false, + children, +}: IProps) { + const { colorClassName, fontWeightClassName } = useMemo(() => { + let colorClass = ""; + let fontWeightClass = "font-semibold"; + + if (isDefault) { + colorClass = "bg-lightDark text-lightDark"; + fontWeightClass = "font-normal"; + } else { + if (status === STATUS_MAPPER.pending) { + colorClass = "bg-error text-error"; + } else if (status === STATUS_MAPPER.ongoing) { + colorClass = "bg-primary text-primary"; + } else if (status === STATUS_MAPPER.completed) { + colorClass = "bg-success text-success"; + } else if (status === STATUS_MAPPER.eligible) { + colorClass = "bg-success text-success"; + fontWeightClass = "font-normal"; + } else if (status === STATUS_MAPPER.available) { + colorClass = "bg-success text-success"; + fontWeightClass = "font-normal"; + } else if (status === STATUS_MAPPER.notAvailable) { + colorClass = "bg-error text-error"; + fontWeightClass = "font-normal"; + } else if (status === STATUS_MAPPER.notVerified) { + colorClass = "bg-error text-error"; + fontWeightClass = "font-normal"; + } + } + + return { colorClassName: colorClass, fontWeightClassName: fontWeightClass }; + }, [status, isDefault]); + + return ( ++ +++ + {children} ++ ); +} diff --git a/components/custom/buttons/Button.tsx b/components/custom/buttons/Button.tsx new file mode 100644 index 0000000..af40260 --- /dev/null +++ b/components/custom/buttons/Button.tsx @@ -0,0 +1,69 @@ +"use client"; +import { ButtonHTMLAttributes, useMemo } from "react"; +import { Button as MTButton } from "@/libraries/material-tailwind"; +import { TColor, TVariant } from "@/utils/types"; + +interface IProps extends ButtonHTMLAttributes{ + variant: TVariant; + color: TColor; +} + +export default function Button({ + variant, + color, + className = "", + children, + onClick, + type, + disabled = false, +}: IProps) { + const _className = useMemo(() => { + let defaultClassName = ""; + if (variant === "filled") { + if (color === "primary") return "border border-primary bg-primary"; + if (color === "secondary") return "border border-secondary bg-secondary"; + if (color === "success") return "border border-success bg-success"; + if (color === "warning") return "border border-warning bg-warning"; + if (color === "error") return "border border-error bg-error"; + if (color === "dark") return "border border-dark bg-dark"; + if (color === "lightDark") return "border border-lightDark bg-lightDark"; + } + + if (variant === "outlined") { + defaultClassName = "bg-transparent border"; + + if (color === "primary") + return `${defaultClassName} border-primary text-primary`; + if (color === "secondary") + return `${defaultClassName} border-secondary text-secondary`; + if (color === "success") + return `${defaultClassName} border-success text-success`; + if (color === "warning") + return `${defaultClassName} border-warning text-warning`; + if (color === "error") + return `${defaultClassName} border-error text-error`; + if (color === "dark") return `${defaultClassName} border-dark text-dark`; + if (color === "lightDark") + return `${defaultClassName} border-lightDark text-lightDark`; + } + + if (variant === "text") { + if (color === "secondary") return "text-secondary"; + } + + return defaultClassName; + }, [variant, color]); + + return ( + + {children} + + ); +} diff --git a/components/custom/buttons/TabButton.tsx b/components/custom/buttons/TabButton.tsx new file mode 100644 index 0000000..a807ff1 --- /dev/null +++ b/components/custom/buttons/TabButton.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { ButtonHTMLAttributes, ReactNode } from "react"; + +interface IProps extends ButtonHTMLAttributes{ + children: ReactNode; + isActive?: boolean; +} + +export default function TabButton({ + className = "", + children, + onClick, + isActive = false, +}: IProps) { + return ( + + ); +} diff --git a/components/custom/calendars/BCalendar/AddEventDialog.tsx b/components/custom/calendars/BCalendar/AddEventDialog.tsx new file mode 100644 index 0000000..687a24d --- /dev/null +++ b/components/custom/calendars/BCalendar/AddEventDialog.tsx @@ -0,0 +1,404 @@ +import Link from "next/link"; +import { ChangeEvent, useMemo, useState } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { Dialog, DialogBody, Typography } from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { SCHEDULE_TIMES } from "@/utils/constants"; +import api from "@/utils/api"; +import { getErrorMessage } from "@/utils/functions"; +import moment from "@/libraries/moment"; +import { Radio } from "@/libraries/material-tailwind"; +import { toast } from "@/libraries/react-toastify"; +import { useCalendar } from "@/contexts/CalendarContext"; +import Select from "@/components/custom/Select"; +import Button from "@/components/custom/buttons/Button"; +import MonthPick from "@/components/custom/MonthPick"; + +interface IProps { + opened: boolean; + setOpened: Function; + date: Date; + size: DialogProps["size"]; +} + +type TAvailiability = "unavailable" | "custom" | "typical"; +type TDateWeek = { + date: number; + weekDay: string; +}; + +export default function AddEventDialog({ + opened, + setOpened, + date, + size, +}: IProps) { + const { setEvents } = useCalendar(); + + const [fromTime, setFromTime] = useState (SCHEDULE_TIMES[0]); + const [toTime, setToTime] = useState (SCHEDULE_TIMES[1]); + const [fromDate, setFromDate] = useState (new Date()); + const [toDate, setToDate] = useState (new Date()); + const [availability, setAvailibility] = + useState ("unavailable"); + + const fromDateWeeks = useMemo >(() => { + const maxDate = new Date( + fromDate.getFullYear(), + fromDate.getMonth() + 1, + 0 + ).getDate(); + const dates = []; + + for (let i = 1; i <= maxDate; i += 1) { + dates.push({ + date: i, + weekDay: moment( + new Date(fromDate.getFullYear(), fromDate.getMonth(), i) + ).format("ddd"), + }); + } + + return dates; + }, [fromDate]); + + const toDateWeeks = useMemo >(() => { + const maxDate = new Date( + toDate.getFullYear(), + toDate.getMonth() + 1, + 0 + ).getDate(); + const dates = []; + + for (let i = 1; i <= maxDate; i += 1) { + dates.push({ + date: i, + weekDay: moment( + new Date(toDate.getFullYear(), toDate.getMonth(), i) + ).format("ddd"), + }); + } + + return dates; + }, [toDate]); + + const handleFrom = (e: ChangeEvent ) => { + setFromTime(e.target.value); + }; + + const handleTo = (e: ChangeEvent ) => { + setToTime(e.target.value); + }; + + const handler = () => { + setOpened(!opened); + }; + + const pickPrevMonthOfFromDate = () => { + setFromDate( + (currentDate) => + new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1) + ); + }; + + const pickNextMonthOfFromDate = () => { + setFromDate( + (currentDate) => + new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1) + ); + }; + + const pickPrevMonthOfToDate = () => { + setToDate( + (currentDate) => + new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1) + ); + }; + + const pickNextMonthOfToDate = () => { + setToDate( + (currentDate) => + new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1) + ); + }; + + const handleSave = () => { + if (availability === "unavailable") { + const day = moment(date).format("YYYY-MM-DD"); + + api + .post("/calendar/set/dates/block", { + startDate: day, + endDate: day, + time: [ + { + start: "00:00:00", + end: "23:59:59", + }, + ], + type: 1, + blockid: "", + }) + .then((res) => { + setEvents((prev) => [ + ...prev, + { + ...res.data.data, + type: "unavailable", + blockId: res.data.data.blockid ? `${res.data.data.blockid}` : "", + }, + ]); + toast.success("Success: An unavailable event has been added."); + setOpened(!opened); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (availability === "custom") { + const startDate = new Date(`${moment(fromDate).format("L")} ${fromTime}`); + const endDate = new Date(`${moment(toDate).format("L")} ${toTime}`); + + if (startDate >= endDate) { + toast.error("Error: Invalid dates."); + } else { + // Should be considered later. + api + .post("/calendar/save/events", { + startDate: moment(startDate).format("YYYY-MM-DD"), + endDate: moment(endDate).format("YYYY-MM-DD"), + time: [ + { + start: moment(startDate).format("HH:mm:ss"), + end: moment(endDate).format("HH:mm:ss"), + }, + ], + type: 1, + blockid: "", + }) + .then((res) => { + setEvents((prev) => [ + ...prev, + { ...res.data.data, type: "available" }, + ]); + toast.success("Success: An event has been added."); + setOpened(!opened); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + } + }; + + return ( + + ); +} diff --git a/components/custom/calendars/BCalendar/DelEventDialog.tsx b/components/custom/calendars/BCalendar/DelEventDialog.tsx new file mode 100644 index 0000000..44cf86c --- /dev/null +++ b/components/custom/calendars/BCalendar/DelEventDialog.tsx @@ -0,0 +1,82 @@ +import { AllHTMLAttributes } from "react"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { Dialog, DialogBody, Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import moment from "@/libraries/moment"; +import { ICON_MAPPER } from "@/utils/constants"; +import { IEvent } from "@/utils/interfaces"; +import Button from "@/components/custom/buttons/Button"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import { useCalendar } from "@/contexts/CalendarContext"; + +interface IProps extends AllHTMLAttributes { + opened: boolean; + setOpened: (value: boolean | ((prev: boolean) => boolean)) => void; + dialogSize: DialogProps["size"]; + event: IEvent; +} + +export default function DelEventDialog({ + opened, + setOpened, + dialogSize, + event, +}: IProps) { + const { setEvents } = useCalendar(); + + const handleDelete = () => { + api + .get(`/calendar/unset/dates/block/${event.id}`) + .then((res) => { + setEvents((prev) => { + const newEvents = [...prev]; + newEvents.splice( + newEvents.findIndex((item) => item.id === event.id), + 1 + ); + return newEvents; + }); + toast.success("Success. The event has been deleted."); + setOpened(false); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }; + + return ( + + ); +} diff --git a/components/custom/calendars/BCalendar/EventItem.tsx b/components/custom/calendars/BCalendar/EventItem.tsx new file mode 100644 index 0000000..fc71abc --- /dev/null +++ b/components/custom/calendars/BCalendar/EventItem.tsx @@ -0,0 +1,10 @@ +import type { EventProps } from "@/libraries/react-big-calendar"; +import { IEvent } from "@/utils/interfaces"; + +export default function EventItem({ title }: EventProps ) { + return ( + ++ ); +} diff --git a/components/custom/calendars/BCalendar/WeekDay.tsx b/components/custom/calendars/BCalendar/WeekDay.tsx new file mode 100644 index 0000000..a48a27b --- /dev/null +++ b/components/custom/calendars/BCalendar/WeekDay.tsx @@ -0,0 +1,29 @@ +import { Typography } from "@/libraries/material-tailwind"; +import type { HeaderProps } from "@/libraries/react-big-calendar"; + +const today = new Date(); + +export default function WeekDay({ date, label }: HeaderProps) { + return ( +{title}
+++ ); +} diff --git a/components/custom/calendars/BCalendar/index.tsx b/components/custom/calendars/BCalendar/index.tsx new file mode 100644 index 0000000..c499336 --- /dev/null +++ b/components/custom/calendars/BCalendar/index.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { AllHTMLAttributes, useMemo, useState } from "react"; +import { + BigCalendar, + EventProps, + momentLocalizer, +} from "@/libraries/react-big-calendar"; +import moment from "@/libraries/moment"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { + Dialog, + DialogBody, + IconButton, + Menu, + MenuHandler, + MenuList, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { useMediaQuery } from "@/libraries/usehooks-ts"; +import { ICON_MAPPER, SCREEN_MAPPER } from "@/utils/constants"; +import WeekDay from "@/components/custom/calendars/BCalendar/WeekDay"; +import EventItem from "@/components/custom/calendars/BCalendar/EventItem"; +import AddEventDialog from "@/components/custom/calendars/BCalendar/AddEventDialog"; +import SCalendar from "@/components/custom/calendars/SCalendar"; +import { useCalendar } from "@/contexts/CalendarContext"; +import { IEvent } from "@/utils/interfaces"; + +interface IProps extends AllHTMLAttributes+ {label} + ++ {label[0]} + +{ + title?: string; +} + +const localizer = momentLocalizer(moment); +const currentDate = new Date(); + +export default function BCalendar({ title = "", className = "" }: IProps) { + const isMd = useMediaQuery(`(max-width: ${SCREEN_MAPPER.md})`); + + const { events } = useCalendar(); + + const [date, setDate] = useState (currentDate); + const [sCalendarOpened, setSCalendarOpened] = useState (false); + const [addEventDialogOpened, setAddEventDialogOpened] = + useState (false); + const [sCalDialogOpened, setSCalDialogOpened] = useState (false); + + const dialogSize = useMemo (() => { + if (isMd) return "xl"; + return "xs"; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSCalendarOpened = () => { + setSCalendarOpened((prev) => !prev); + }; + + const gotoNeighborMonth = (toNext: boolean) => { + const monthOffset = toNext ? 1 : -1; + setDate( + (prev) => new Date(prev.getFullYear(), prev.getMonth() + monthOffset, 1) + ); + }; + + const gotoNeighborDate = (toNext: boolean) => { + const dateOffset = toNext ? 1 : -1; + setDate( + (prev) => + new Date( + prev.getFullYear(), + prev.getMonth(), + prev.getDate() + dateOffset + ) + ); + }; + + const onClickDate = (dt: Date) => { + setDate(dt); + setAddEventDialogOpened(true); + }; + + return ( + <> + ++ +++ ++ {title || moment(date).format("MMMM, YYYY")} +
+ +++ +gotoNeighborDate(false)} + > + + + + ++ gotoNeighborDate(true)} + > + ++ setSCalDialogOpened(true)} + > + ++ <>>, + month: { + header: WeekDay, + event: (props: EventProps ) => , + }, + }} + onNavigate={onClickDate} + /> + + + + > + ); +} diff --git a/components/custom/calendars/SCalendar.tsx b/components/custom/calendars/SCalendar.tsx new file mode 100644 index 0000000..cf3f7a3 --- /dev/null +++ b/components/custom/calendars/SCalendar.tsx @@ -0,0 +1,107 @@ +import { AllHTMLAttributes, ForwardedRef, forwardRef, useMemo } from "react"; +import { ICON_MAPPER, PATH_MAPPER, SHORT_WEEKDAYS } from "@/utils/constants"; +import { Icon } from "@/libraries/iconify-react"; +import { IconButton } from "@/libraries/material-tailwind"; +import { Calendar } from "@/libraries/react-calendar"; +import Button from "@/components/custom/buttons/Button"; +import Link from "next/link"; + +interface IProps extends AllHTMLAttributes { + date: Date; + gotoNeighborMonth: (toNext: boolean) => void; + isAddSchedule: boolean; +} + +const SCalendar = forwardRef ( + ( + { className = "", date, gotoNeighborMonth, isAddSchedule = true }: IProps, + ref: ForwardedRef , + ) => { + const monthYear = useMemo(() => { + const formatter = new Intl.DateTimeFormat("en-us", { + year: "numeric", + month: "long", + }); + return formatter.format(date).replaceAll(".", ""); + }, [date]); + + return ( + + {isAddSchedule ? ( ++ ); + }, +); + +SCalendar.displayName = "SCalendar"; + +export default SCalendar; diff --git a/components/dashboard/AvatarCard.tsx b/components/dashboard/AvatarCard.tsx new file mode 100644 index 0000000..5d144df --- /dev/null +++ b/components/dashboard/AvatarCard.tsx @@ -0,0 +1,193 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { IComponent } from "@/utils/interfaces"; +import { COLOR_MAPPER, ICON_MAPPER } from "@/utils/constants"; +import { Avatar, Typography } from "@/libraries/material-tailwind"; +import { CircularProgressbarWithChildren } from "@/libraries/react-circular-progressbar"; +import { Icon } from "@/libraries/iconify-react"; +import CardTemplate from "@/components/custom/CardTemplate"; +import { useUser } from "@/contexts/UserContext"; +import EmptyAvatar from "../custom/EmptyAvatar"; +import api from "@/utils/api"; +import { error } from "console"; +import { toast } from "react-toastify"; + +export default function AvatarCard({ className = "" }: IComponent) { + const { userData } = useUser(); + + const [selectPermanentJob, setSelectPermanentJob] = useState++ ) : ( +Calendar
+++gotoNeighborMonth(false)} + > + ++ {monthYear}
+gotoNeighborMonth(true)} + > + ++ ++ )} + +gotoNeighborMonth(false)} + > + ++ {monthYear}
+gotoNeighborMonth(true)} + > + ++ ++SHORT_WEEKDAYS[date.getDay()]} + calendarType="gregory" + value={date} + /> + {isAddSchedule ? ( + + + + ) : ( + + )} + (false); + const [selectTempJob, setSelectTempJob] = useState (false); + + const handleSetJobType = (type: string): void => { + let jobTypes: string[] = []; + + if (type == "fulltime") { + !selectPermanentJob && jobTypes.push("permanent"); + selectTempJob && jobTypes.push("temporary"); + } else { + selectPermanentJob && jobTypes.push("permanent"); + !selectTempJob && jobTypes.push("temporary"); + } + + api + .post("/professional/dashboard/jobtype", { + jobType: jobTypes, + }) + .then((res) => { + const { success } = res.data; + if (success) { + toast.success("Succeed in setting a jobType"); + } + }) + .catch((err) => { + toast.error("Failed in setting a jobType."); + }); + }; + + const handlePermanentJobClick = () => { + setSelectPermanentJob(!selectPermanentJob); + handleSetJobType("fulltime"); + }; + + const handleTempJobClick = () => { + setSelectTempJob(!selectTempJob); + handleSetJobType("temporary"); + }; + + useEffect(() => { + api + .get("/professional/dashboard/jobtype") + .then((res) => { + let jobTypes: string[] = res.data.jobType; + jobTypes?.forEach((type) => { + type == "permanent" && setSelectPermanentJob(true); + type == "temporary" && setSelectTempJob(true); + }); + }) + .catch((error) => { + toast.error("Failed in getting a jobType."); + }); + }, []); + + return ( + + + ); +} diff --git a/components/dashboard/BookingsCard.tsx b/components/dashboard/BookingsCard.tsx new file mode 100644 index 0000000..d928168 --- /dev/null +++ b/components/dashboard/BookingsCard.tsx @@ -0,0 +1,119 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import { IBooking, IComponent } from "@/utils/interfaces"; +import { PATH_MAPPER } from "@/utils/constants"; +import CardTemplate from "@/components/custom/CardTemplate"; +import TabButton from "@/components/custom/buttons/TabButton"; +import { List, ListItem, Typography } from "@/libraries/material-tailwind"; +import StatusBadge from "@/components/custom/StatusBadge"; +import moment from "@/libraries/moment"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { getErrorMessage } from "@/utils/functions"; + + +interface IBookings { + id: number; + title: string; + bookedAt: string; + status: string; +} + +export default function BookingsCard({ className = "" }: IComponent) { + const [activeTab, setActiveTab] = useState++++ +++ ++ {userData?.avatar ? ( + ++ ) : ( + + )} + +++ 40% Completed + +++ ++ Welcome to Dental Jobs! + ++ Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida + pulvinar tempus purus. Feugiat quam aliquam. + ++++ Availablility + + +++++ ++++ + Permanent Job + ++ +++++ + Temporary Job + ++ ("Today"); + const [bookings, setBookings] = useState (); + + function filterBookings() { + switch (activeTab) { + case "Today": + return bookings; + case "Past": + return bookings?.filter((booking) => booking.status === "Completed"); + case "Future": + return bookings?.filter((booking) => booking.status === "Pending"); + default: + return bookings; + } + } + + + useEffect(() => { + api + .post("/jobs/professional/dashboard/bookings") + .then((res) => { + setBookings(res.data); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }, []); + + return ( + + See All + + } + > + + ); +} diff --git a/components/dashboard/CalendarCard.tsx b/components/dashboard/CalendarCard.tsx new file mode 100644 index 0000000..ccbcffa --- /dev/null +++ b/components/dashboard/CalendarCard.tsx @@ -0,0 +1,29 @@ +"use client"; +import { AllHTMLAttributes, useState } from "react"; +import { Card, CardBody } from "@/libraries/material-tailwind"; +import SCalendar from "@/components/custom/calendars/SCalendar"; + +export default function CalendarCard({ + className = "", +}: AllHTMLAttributes++++ +setActiveTab("Today")} + > + Today + +setActiveTab("Future")} + > + Future + +setActiveTab("Past")} + > + Past + ++ {filterBookings()?.map((b, i) => ( +
++ + ))} +++ ++ {b.title} + ++ {moment(b.bookedAt).format("llll")} + +{b.status} +) { + const [date, setDate] = useState (new Date()); + + const gotoNeighborMonth = (toNext: boolean) => { + const monthOffset = toNext ? 1 : -1; + setDate( + (prev) => new Date(prev.getFullYear(), prev.getMonth() + monthOffset, 1), + ); + }; + + return ( + + + ); +} diff --git a/components/dashboard/CompleteProfileCard/AddLicenseNum.tsx b/components/dashboard/CompleteProfileCard/AddLicenseNum.tsx new file mode 100644 index 0000000..e707f44 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/AddLicenseNum.tsx @@ -0,0 +1,157 @@ +import React, { useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; + +import { + Dialog, + DialogBody, + IconButton, + Typography, + MTInput, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends IComponent { + addLicenseNumDialog: boolean; + setAddLicenseNumDialog: Function; + size: DialogProps["size"]; +} + +export default function AddLicenseDialog({ + addLicenseNumDialog, + setAddLicenseNumDialog, + size = "md", +}: IProps) { + const handler = () => { + setAddLicenseNumDialog(!addLicenseNum); + }; + + const [loading, setLoading] = useState+ ++ (false); + const [licenseNum, setLicenseNum] = useState (); + const { userData, setUserData } = useUser(); + + const handleChange = (e: any) => { + setLicenseNum(e.target.value); + }; + + const addLicenseNum = (result: boolean) => { + setLoading(true); + api + .post("/user/save/profile/license", { result, licenseNum }) + .then((res) => { + setLoading(false); + let tempUserData = userData; + Object(tempUserData?.verifyData).licenseAdded = true; + setUserData(tempUserData); + toast.success("Successfully added"); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + setAddLicenseNumDialog(!addLicenseNum); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/BackgroundCheckDialog.tsx b/components/dashboard/CompleteProfileCard/BackgroundCheckDialog.tsx new file mode 100644 index 0000000..efc6bfe --- /dev/null +++ b/components/dashboard/CompleteProfileCard/BackgroundCheckDialog.tsx @@ -0,0 +1,349 @@ +"use client"; + +import React, { useState } from 'react' +import { IComponent } from '@/utils/interfaces'; +import { DialogProps } from '@material-tailwind/react' +import { + Dialog, + DialogBody, + IconButton, + Typography, + MTInput, + } from "@/libraries/material-tailwind"; +import Loading from '@/components/custom/Loading'; +import { Icon } from '@/libraries/iconify-react'; +import { ICON_MAPPER } from '@/utils/constants'; +import Button from '@/components/custom/buttons/Button'; +import api from '@/utils/api'; +import { toast } from '@/libraries/react-toastify'; +import { getErrorMessage } from '@/utils/functions'; + +const LIST_CRIMINAL_SEARCH = [ + { + id: 1, + label: "Records with less-than-misdemeanor severity", + }, + { + id: 2, + label: "Records when you were under 18", + }, + { + id: 3, + label: "Misdemeanor:", + }, + { + id: 4, + label: "Deferred / alternative adjudication records", + }, + { + id: 5, + label: "Marijuana possession records", + }, + { + id: 6, + label: "Vehicles & traffic records", + }, + { + id: 7, + label: "Public nuisance records", + }, + { + id: 8, + label: "Alcohol & Tobacco", + }, + { + id: 9, + label: "Marijuana Possession/Use", + }, + { + id: 10, + label: "Driving under the Influence (DUI)", + }, +] + +interface IProps extends IComponent { + backgroundCheck: boolean; + setBackgroundCheck: Function; + size: DialogProps["size"]; + } + +export default function BackgroundCheckDialog({ + backgroundCheck, + setBackgroundCheck, + size = "md" +} : IProps) { + + const handler = () => { + setBackgroundCheck(!backgroundCheck); + }; + + let loading = false; + + const [cardNumber, setCardNumber] = useState (""); + const [expiry, setExpiry] = useState (""); + const [pin, setPin] = useState (""); + + const handleChange = (e: any) => { + const { id, value } = e.target; + + if( id === "cardNum"){ + setCardNumber(value); + } else if ( id === "expDate"){ + setExpiry(value); + } else { + setPin(value); + } + }; + + const handleSaved = () => { + api + .post("/membership/save/credit/card", { + cardNumber: cardNumber, + expiry: expiry, + pin: pin + } ) + .then((res) => { + toast.success("Saved successfully."); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + + setCardNumber(""); + setExpiry(""); + setPin(""); + }; + + return ( + + ) +} + diff --git a/components/dashboard/CompleteProfileCard/BackgroundDescDialog.tsx b/components/dashboard/CompleteProfileCard/BackgroundDescDialog.tsx new file mode 100644 index 0000000..bd936f2 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/BackgroundDescDialog.tsx @@ -0,0 +1,201 @@ +"use client"; + +import React, { ChangeEvent, useEffect, useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + IconButton, + Typography, + Textarea, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import Loading from "@/components/custom/Loading"; +import { toast } from "react-toastify"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends IComponent { + backgroundDesc: boolean; + setBackgroundDesc: Function; + size: DialogProps["size"]; +} + +export default function BackgroundDescDialog({ + backgroundDesc, + setBackgroundDesc, + size = "md", +}: IProps) { + const handler = () => { + setBackgroundDesc(!backgroundDesc); + }; + + const [loading, setLoading] = useState (false); + const [description, setDescription] = useState (""); + const { userData, setUserData } = useUser(); + const handleChange = (e: ChangeEvent ) => { + setDescription(e.target.value); + }; + + useEffect(() => { + setLoading(true); + api + .get("/user/get/profile/description") + .then((res) => { + setLoading(false); + setDescription(res.data.description); + }) + .catch((err) => { + setLoading(false); + console.log(err); + }); + }, []); + + const handleSave = () => { + setLoading(true); + api + .post("/user/save/profile/description", { description: description }) + .then((res) => { + setLoading(false); + toast.success("Saved successfully."); + let tempUserData = userData; + Object(tempUserData?.verifyData).backgroundDescAdded = true; + setUserData(tempUserData); + setBackgroundDesc(!backgroundDesc); + }) + .catch((err) => { + setLoading(false); + console.log(err); + }); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/DentalTrainingDialog.tsx b/components/dashboard/CompleteProfileCard/DentalTrainingDialog.tsx new file mode 100644 index 0000000..e29d165 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/DentalTrainingDialog.tsx @@ -0,0 +1,191 @@ +"use client"; + +import React, { FormEvent, useEffect, useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import Input from "@/components/custom/Input"; +import Select from "@/components/custom/Select"; +import Checkbox from "@/components/custom/Checkbox"; +import { IComponent, ITraining } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER, VALIDATION_REQUIRED_FIELD } from "@/utils/constants"; + +import { + Dialog, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import yup from "@/libraries/yup"; +import { useFormik } from "@/libraries/formik"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import Loading from "@/components/custom/Loading"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends IComponent { + dentalTraining: boolean; + setDentalTraining: Function; + size: DialogProps["size"]; +} + +let gradYears: number[] = []; +let currentYear = new Date().getFullYear(); +for (let index = currentYear - 100; index <= currentYear + 10; index++) { + gradYears.push(index); +} + +const validationSchema = yup.object().shape({ + school: yup.string().required(VALIDATION_REQUIRED_FIELD), + gradYear: yup.string().required(VALIDATION_REQUIRED_FIELD), +}); + +const initValues: ITraining = { + school: "", + gradYear: `${currentYear - 10}`, + graduated: false, +}; + +export default function DentalTrainingDialog({ + dentalTraining, + setDentalTraining, + size = "md", +}: IProps) { + const [loading, setLoading] = useState (false); + const { userData, setUserData } = useUser(); + + const formik = useFormik({ + initialValues: initValues, + validationSchema, + onSubmit: (values) => { + setLoading(true); + api + .post("/user/save/profile/training", values) + .then((res) => { + setLoading(false); + if (res.data.success) { + toast.success("Saved."); + let tempUserData = userData; + Object(tempUserData?.verifyData).trainingAdded = true; + setUserData(tempUserData); + handler(); + } + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }, + }); + + const handler = () => { + setDentalTraining(!dentalTraining); + }; + + const handleSubmit = (e: FormEvent ) => { + e.preventDefault(); + formik.handleSubmit(); + }; + + useEffect(() => { + api + .get("/user/get/profile/training") + .then((res) => { + formik.setValues(res.data); + }) + .catch((err) => {}); + }, []); + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/LanguagesDialog.tsx b/components/dashboard/CompleteProfileCard/LanguagesDialog.tsx new file mode 100644 index 0000000..a98f65d --- /dev/null +++ b/components/dashboard/CompleteProfileCard/LanguagesDialog.tsx @@ -0,0 +1,275 @@ +"use client"; + +import React, { ChangeEvent, useEffect, useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import Select from "@/components/custom/Select"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import Loading from "@/components/custom/Loading"; +import { getErrorMessage } from "@/utils/functions"; +import { toast } from "react-toastify"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends IComponent { + languagesDialog: boolean; + setLanguagesDialog: Function; + size: DialogProps["size"]; +} + +interface ILanguageItem { + id: string; + level: number; +} + +interface ILanguageName { + id: number, + title: string, +} + +const levels = [ + { + level: 11, + name: "Beginer", + }, + { + level: 12, + name: "Intermediate", + }, + { + level: 13, + name: "Advanced", + }, + { + level: 14, + name: "Professional", + }, +]; + +export default function LanguagesDialog({ + languagesDialog, + setLanguagesDialog, + size = "md", +}: IProps) { + const handler = () => { + setLanguagesDialog(!languagesDialog); + }; + + const [loading, setLoading] = useState (false); + const [langNames, setLangNames] = useState< + Array<{ id: string; title: string }> + >([]); + const [languages, setLanguages] = useState >([]); + const { userData, setUserData } = useUser(); + + useEffect(() => { + (async () => { + setLoading(true); + await api + .get("/get/common/datas/lang") + .then((res) => { + res.data.sort((A: ILanguageName, B: ILanguageName) => { return A.id - B.id}); + setLangNames(res.data); + setLoading(false); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + + await api + .get("/user/get/profile/languages") + .then((res) => { + setLanguages(res.data); + setLoading(false); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + })(); + }, []); + + const handleDone = () => { + const requestData = { languages: languages }; + api + .post("/user/save/profile/languages", requestData) + .then(() => { + setLoading(false); + let tempUserData = userData; + Object(tempUserData?.verifyData).languageAdded = true; + setUserData(tempUserData); + toast.success("Uploaded successfully."); + setLanguagesDialog(!languagesDialog); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }; + + const handleSelectLanguageName = ( + e: ChangeEvent , + index: number + ) => { + + const langId = e.target.value; + setLanguages((prev) => { + const prevLangs = [...prev]; + prevLangs[index].id = langId; + return prevLangs; + }); + }; + + const handleSelectLevel = ( + e: ChangeEvent , + index: number + ) => { + const newLevel = e.target.value ; + + setLanguages((prev) => { + const prevLangs = [...prev]; + prevLangs[index].level = Number(newLevel); + return prevLangs; + + }) + }; + + const handleDeleteLanguage = (index: number) => { + setLanguages((prev) => { + const prevLanguages = [...prev]; + prevLanguages.splice(index, 1); + + return prevLanguages; + }); + }; + + const handleAddLanguage = () => { + setLanguages((prev) => [ + ...prev, + { + id: langNames[0].id, + level: levels[0].level, + }, + ]); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/ListSkillsDialog.tsx b/components/dashboard/CompleteProfileCard/ListSkillsDialog.tsx new file mode 100644 index 0000000..6cdff7d --- /dev/null +++ b/components/dashboard/CompleteProfileCard/ListSkillsDialog.tsx @@ -0,0 +1,281 @@ +"use client"; + +import React, { ChangeEvent, useState, useEffect } from "react"; +import Button from "@/components/custom/buttons/Button"; +import Input from "@/components/custom/Input"; +import Select from "@/components/custom/Select"; +import Checkbox from "@/components/custom/Checkbox"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import Loading from "@/components/custom/Loading"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends IComponent { + listSkillsDialog: boolean; + setListSkillsDialog: Function; + size: DialogProps["size"]; +} + +interface IDentalVisionLevel { + id: number; + level: string; +} + +interface ISkill { + id: number; + title: string; + level: { + id: number; + title: string; + }; +} + +const DENTAL_VISION_LEVEL: IDentalVisionLevel[] = [ + { + id: 1, + level: "Begineer", + }, + { + id: 2, + level: "Intermediate", + }, + { + id: 3, + level: "Advanced", + }, + { + id: 4, + level: "Professional", + }, +]; + +export default function ListSkillsDialog({ + listSkillsDialog, + setListSkillsDialog, + size = "md", +}: IProps) { + const handler = () => { + setListSkillsDialog(!listSkillsDialog); + }; + + const [loading, setLoading] = useState (false); + const { userData, setUserData } = useUser(); + + useEffect(() => { + setLoading(true); + api + .get("/user/get/profile/skills") + .then((res) => { + setLoading(false); + setSkills(res.data); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }, []); + + const handleNext = () => { + setLoading(true); + api + .post("/user/save/profile/skills", skills) + .then((res) => { + setLoading(false); + let tempUserData = userData; + Object(tempUserData?.verifyData).skillsAdded = true; + setUserData(tempUserData); + toast.success("Saved successfully"); + setListSkillsDialog(!listSkillsDialog); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }; + + const [skills, setSkills] = useState ([]); + const [newSkill, setNewSkill] = useState (""); + const [level, setLevel] = useState (DENTAL_VISION_LEVEL[0].level); + + const handleAdd = () => { + const trimmedNewSkill = newSkill.trim(); + if (trimmedNewSkill === "") { + return; + } + const skillExists = skills.some( + (skill) => skill.title.toLowerCase() === trimmedNewSkill.toLowerCase() + ); + if (skillExists) { + toast.error("That already exists."); + } else { + const newSkillItem: ISkill = { + id: skills.length + 1, + title: trimmedNewSkill, + level: { + id: 1, + title: "Beginner", + }, + }; + setSkills([...skills, newSkillItem]); + setNewSkill(""); + } + }; + + const handleInputChange = (e: ChangeEvent ) => { + setNewSkill(e.target.value); + }; + const handleInputKeyPress = (e: React.KeyboardEvent ) => { + if (e.key === "Enter") { + handleAdd(); + } + }; + + const handleDelete = (id: number) => { + const updatedSkills = skills.filter((skill) => skill.id !== id); + setSkills(updatedSkills); + }; + + const handleSelect = (e: ChangeEvent ) => { + setLevel(e.target.value); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/UploadProfilePicDialog.tsx b/components/dashboard/CompleteProfileCard/UploadProfilePicDialog.tsx new file mode 100644 index 0000000..c76b958 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/UploadProfilePicDialog.tsx @@ -0,0 +1,334 @@ +import { useState, ChangeEvent, useRef } from "react"; +import Image from "next/image"; +import { + Dialog, + DialogBody, + Avatar, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { toast } from "@/libraries/react-toastify"; +import api from "@/utils/api"; +import { ICON_MAPPER } from "@/utils/constants"; +import { getErrorMessage } from "@/utils/functions"; +import { IComponent } from "@/utils/interfaces"; +import { useUser } from "@/contexts/UserContext"; +import Loading from "@/components/custom/Loading"; +import Button from "@/components/custom/buttons/Button"; + +interface IProps extends IComponent { + uploadProfilePic: boolean; + setUploadProfilePic: React.Dispatch >; + size: DialogProps["size"]; +} + +export default function UploadProfilePicDialog({ + uploadProfilePic, + setUploadProfilePic, + size = "md", +}: IProps) { + const handleClose = () => { + setUploadProfilePic(false); + }; + + const { userData, setUserData } = useUser(); + const [profilePhoto, setProfilePhoto] = useState (null); + const [profilePhotoSrc, setProfilePhotoSrc] = useState ( + userData?.avatar ? userData.avatar : "" + ); + const [photoID, setPhotoID] = useState (null); + const [photoIDSrc, setPhotoIDSrc] = useState (""); + const profileInputFile = useRef (null); + const photoIDInputFile = useRef (null); + const [loading, setLoading] = useState (false); + + const openProfilePhotoWindow = () => { + profileInputFile.current?.click(); + }; + + const openPhotoIDWindow = () => { + photoIDInputFile.current?.click(); + }; + + const selectProfilePhoto = (e: ChangeEvent ) => { + if (e.target.files && e.target.files[0]) { + setProfilePhoto(e.target.files[0]); + setProfilePhotoSrc(URL.createObjectURL(e.target.files[0])); + } + }; + + const selectPhotoID = (e: ChangeEvent ) => { + if (e.target.files && e.target.files[0]) { + setPhotoID(e.target.files[0]); + setPhotoIDSrc(URL.createObjectURL(e.target.files[0])); + } + }; + + const handleSubmit = (e: ChangeEvent ) => { + e.preventDefault(); + setLoading(true); + + const formData = new FormData(); + profilePhoto && formData.append("avatar", profilePhoto); + photoID && formData.append("idImg", photoID); + + const config = { + headers: { + "content-type": "multipart/form-data", + }, + }; + + api + .post("/user/upload/profile/photos", formData, config) + .then((res) => { + setUserData((prev) => + prev + ? { + ...prev, + avatar: res.data.profilePhoto, + } + : prev + ); + setLoading(false); + toast.success("Uploaded."); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/VerifyEmailDialog.tsx b/components/dashboard/CompleteProfileCard/VerifyEmailDialog.tsx new file mode 100644 index 0000000..0bbf161 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/VerifyEmailDialog.tsx @@ -0,0 +1,133 @@ +import React, { useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { useUser } from "@/contexts/UserContext"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import Loading from "@/components/custom/Loading"; + +interface IProps extends IComponent { + verifyEmail: boolean; + setVerifyEmail: Function; + size: DialogProps["size"]; +} + +export default function VerifyEmailDialog({ + verifyEmail, + setVerifyEmail, + size = "md", +}: IProps) { + const handler = () => { + setVerifyEmail(!verifyEmail); + }; + + const { userData, setUserData } = useUser(); + const [loading, setLoading] = useState (false); + const resendConfirmationMail = () => { + setLoading(true); + api + .get("/user/send/email/verification") + .then((res) => { + setLoading(false); + let tempUserData = userData; + Object(tempUserData?.verifyData).emailConfirmed = true; + setUserData(tempUserData); + toast.success("Confirmation Mail was successfully sent to your email"); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + setVerifyEmail(!verifyEmail); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/OTPForm.tsx b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/OTPForm.tsx new file mode 100644 index 0000000..3484463 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/OTPForm.tsx @@ -0,0 +1,88 @@ +import { Dispatch, FormEvent, FormHTMLAttributes, SetStateAction } from "react"; +import Input from "@/components/custom/Input"; +import { useFormik } from "@/libraries/formik"; +import yup from "@/libraries/yup"; +import { VALIDATION_REQUIRED_FIELD } from "@/utils/constants"; +import Button from "@/components/custom/buttons/Button"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import { useUser } from "@/contexts/UserContext"; + +interface IProps extends FormHTMLAttributes { + setOtpSent: Dispatch >; + setLoading: Dispatch >; + handler: () => void; + phone: string; +} + +export default function OTPForm({ + setOtpSent, + setLoading, + handler, + phone, + ...others +}: IProps) { + const { userData, setUserData } = useUser(); + const formik = useFormik({ + initialValues: { + otp: "", + }, + validationSchema: yup.object().shape({ + otp: yup.string().required(VALIDATION_REQUIRED_FIELD), + }), + onSubmit: ({ otp }) => { + setLoading(true); + api + .post("/user/verify/phone/number", { + phone: phone.slice(2), + code: otp, + }) + .then(() => { + setLoading(false); + setOtpSent(true); + let tempUserData = userData; + Object(tempUserData?.verifyData).phoneConfirmed = true; + setUserData(tempUserData); + handler(); + toast.success("You phone has been verified."); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }, + }); + + const resendCode = () => { + setOtpSent(false); + }; + + const handleSubmit = (e: FormEvent ) => { + e.preventDefault(); + formik.handleSubmit(); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/PhoneForm.tsx b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/PhoneForm.tsx new file mode 100644 index 0000000..89b3ea4 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/PhoneForm.tsx @@ -0,0 +1,76 @@ +"use client"; +import { + Dispatch, + FormEvent, + FormHTMLAttributes, + SetStateAction, + useEffect, +} from "react"; +import Button from "@/components/custom/buttons/Button"; +import { PhoneInput } from "@/libraries/react-phone-number-input"; +import { useUser } from "@/contexts/UserContext"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { getErrorMessage } from "@/utils/functions"; + +interface IProps extends FormHTMLAttributes { + setOtpSent: Dispatch >; + setLoading: Dispatch >; + setPhone: Dispatch >; + phone: string; +} + +export default function PhoneForm({ + setOtpSent, + setLoading, + setPhone, + phone, + ...others +}: IProps) { + const { userData } = useUser(); + + const handleSubmit = (e: FormEvent ) => { + e.preventDefault(); + setLoading(true); + api + .post("/user/send/phone/verification", { phone: phone.slice(2) }) + .then((res) => { + setLoading(false); + toast.success(`OTP: ${res.data.code}`); + setOtpSent(true); + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }; + + useEffect(() => { + if (userData?.phone) { + const onlyNumbers = userData.phone.replace(/\D/g, ""); + setPhone(`+1${onlyNumbers}`); + } + }, [userData?.phone]); + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/index.tsx b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/index.tsx new file mode 100644 index 0000000..1029453 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/VerifyPhoneDialog/index.tsx @@ -0,0 +1,75 @@ +"use client"; + +import React, { useState } from "react"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + DialogHeader, + IconButton, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import Loading from "@/components/custom/Loading"; +import PhoneForm from "./PhoneForm"; +import OTPForm from "./OTPForm"; + +interface IProps extends IComponent { + verifyPhone: boolean; + setVerifyPhone: Function; + size: DialogProps["size"]; +} + +export default function VerifyPhoneDialog({ + verifyPhone, + setVerifyPhone, + size = "md", +}: IProps) { + const [loading, setLoading] = useState (false); + const [otpSent, setOtpSent] = useState (true); + const [phone, setPhone] = useState (""); + + const handler = () => { + setVerifyPhone(!verifyPhone); + }; + + return ( + + ); +} diff --git a/components/dashboard/CompleteProfileCard/index.tsx b/components/dashboard/CompleteProfileCard/index.tsx new file mode 100644 index 0000000..b3c3982 --- /dev/null +++ b/components/dashboard/CompleteProfileCard/index.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { IComponent } from "@/utils/interfaces"; +import CardTemplate from "@/components/custom/CardTemplate"; +import { Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import DentalTrainingDialog from "./DentalTrainingDialog"; +import VerifyEmailDialog from "./VerifyEmailDialog"; +import VerifyPhoneDialog from "./VerifyPhoneDialog"; +import AddLicenseDialog from "./AddLicenseNum"; +import ListSkillsDialog from "./ListSkillsDialog"; +import BackgroundDescDialog from "./BackgroundDescDialog"; +import LanguagesDialog from "./LanguagesDialog"; +import UploadProfilePicDialog from "./UploadProfilePicDialog"; +import BackgroundCheckDialog from "./BackgroundCheckDialog"; +import { useUser } from "@/contexts/UserContext"; +import api from "@/utils/api"; +import { useAuth } from "@/contexts/AuthContext"; +import { LINKS_TO_COMPLETE } from "@/utils/constants"; + +export default function CompleteProfileCard({ className = "" }: IComponent) { + const [dentalTraining, setDentalTraining] = useState (false); + const [verifyEmail, setVerifyEmail] = useState (false); + const [verifyPhone, setVerifyPhone] = useState (false); + const [addLicenseNumDialog, setAddLicenseNumDialog] = + useState (false); + const [listSkillsDialog, setListSkillsDialog] = useState (false); + const [backgroundDesc, setBackgroundDesc] = useState (false); + const [languagesDialog, setLanguagesDialog] = useState (false); + const [uploadProfilePic, setUploadProfilePic] = useState (false); + const [backgroundCheck, setBackgroundCheck] = useState (false); + const { userData, setUserData } = useUser(); + const { authToken } = useAuth(); + + const handleLINKS_TO_COMPLETE = useCallback((title: string) => { + switch (title) { + case "Dental Training": + setDentalTraining(true); + break; + case "Verify Your Email Address": + setVerifyEmail(true); + break; + case "Verify Your Mobile Phone Number": + setVerifyPhone(true); + break; + case "Add Your License Number": + setAddLicenseNumDialog(true); + break; + case "List Your Skills": + setListSkillsDialog(true); + break; + case "Background Description": + setBackgroundDesc(true); + break; + case "Languages Your Speak": + setLanguagesDialog(true); + break; + case "Upload Your Profile Picture & Photo ID": + setUploadProfilePic(true); + break; + case "Complete A Background Check (Optional)": + setBackgroundCheck(true); + break; + default: + break; + } + }, []); + + useEffect(() => { + api + .get("/professional/get/profile/info") + .then((res) => { + setUserData({ ...userData, ...res.data }); + }) + .catch(() => {}); + }, [authToken]); + + return ( + <> + + ++ {LINKS_TO_COMPLETE.map((link) => ( ++handleLINKS_TO_COMPLETE(link.label)} + > ++ ))} ++ + {link.label} + ++ + + + + + + {languagesDialog && ( + + )} + {uploadProfilePic && ( + + )} + > + ); +} diff --git a/components/dashboard/JobPostingCard/JPCardTemplate.tsx b/components/dashboard/JobPostingCard/JPCardTemplate.tsx new file mode 100644 index 0000000..be9918b --- /dev/null +++ b/components/dashboard/JobPostingCard/JPCardTemplate.tsx @@ -0,0 +1,178 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { Avatar, Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { IComponent, IJob } from "@/utils/interfaces"; +import { formatNumber } from "@/utils/functions"; +import JobDescriptionDialog from "./JobDescriptionDialog"; +import JobApplyDialog from "./JobApplyDialog"; +import moment from "moment"; +import Loading from "@/components/custom/Loading"; + +interface IProps extends IComponent { + job: IJob; + activeTab: string; +} + +export default function JPCardTemplate({ + className = "", + job, + activeTab, +}: IProps) { + const [viewJob, setViewJob] = useState (false); + const [applyJob, setApplyJob] = useState (false); + + const handleViewJob = () => { + setViewJob(true); + }; + const handleApplyJob = () => { + setApplyJob(true); + }; + + return ( + job ? + ++ :++ +++ + {/*+ {job.title} + + {/*+ {job.poster.organization} + */} +*/} + ++ ++++ {/* + {job.poster.rate} ({job.poster.numberOfReviews} Reviews) + */} +++ ++ + {job.experience} + +++ ++ + {job.salary === null + ? "None" + : `$${job.salary?.min + }-$${job.salary?.max} ${ + job.salary?.mode + }`} + +++ + {/*+ + {job.location || "None"} + ++*/} + ++ + {job.requirement.skillMatchRate.matchedCount} of{" "} + {job.requirement.skillMatchRate.total} skills matched + ++++ + {moment(job.postedAt).format("MM/DD/YYYY hh:mm a")} + ++ + +++ + No Job Posting found here+ ); +} diff --git a/components/dashboard/JobPostingCard/JobApplyDialog.tsx b/components/dashboard/JobPostingCard/JobApplyDialog.tsx new file mode 100644 index 0000000..e5d3786 --- /dev/null +++ b/components/dashboard/JobPostingCard/JobApplyDialog.tsx @@ -0,0 +1,174 @@ +import Image from "next/image"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent, IJob } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER, PATH_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + Typography, + DialogProps, + IconButton, + Avatar, + Textarea, +} from "@/libraries/material-tailwind"; +import { ellipsisString, getErrorMessage } from "@/utils/functions"; +import moment from "moment"; +import { ChangeEvent, useState } from "react"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +interface IProps extends IComponent { + applyJob: boolean; + setApplyJob: React.Dispatch>; + job: IJob; + size: DialogProps["size"]; +} + +const JobApplyDialog: React.FC = ({ + applyJob, + setApplyJob, + job, + size, +}: IProps) => { + const router = useRouter(); + + const handler = () => { + setApplyJob(!applyJob); + }; + + const handleApply = () => { + + const applyForJob = async () => { + await api + .post("/jobs/professional/send/application", { + jobId: job.id, + message: message + }) + .then((res) => { + setApplyJob(!applyJob); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + applyForJob(); + setMessage(""); + } + + const [message, setMessage] = useState (""); + + const handleChange = (e: ChangeEvent ) => { + setMessage(e.target.value); + }; + + return ( + + ); +}; + +export default JobApplyDialog; diff --git a/components/dashboard/JobPostingCard/JobDescriptionDialog.tsx b/components/dashboard/JobPostingCard/JobDescriptionDialog.tsx new file mode 100644 index 0000000..16068c3 --- /dev/null +++ b/components/dashboard/JobPostingCard/JobDescriptionDialog.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { IComponent, IJob } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import JobTabItem from "@/components/job-posting/JobTabItem"; + +interface IProps extends IComponent { + viewJob: boolean; + setViewJob: React.Dispatch >; + job: IJob; + activeTab: string; + size: DialogProps["size"]; +} + +const JobDescriptionDialog: React.FC = ({ + viewJob, + setViewJob, + job, + activeTab, + size, +}: IProps) => { + const handler = () => { + setViewJob(!viewJob); + }; + + return ( + <> + + > + ); +}; + +export default JobDescriptionDialog; diff --git a/components/dashboard/JobPostingCard/index.tsx b/components/dashboard/JobPostingCard/index.tsx new file mode 100644 index 0000000..b052ecd --- /dev/null +++ b/components/dashboard/JobPostingCard/index.tsx @@ -0,0 +1,113 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import { IComponent, IJob } from "@/utils/interfaces"; +import { PATH_MAPPER } from "@/utils/constants"; +import CardTemplate from "@/components/custom/CardTemplate"; +import TabButton from "@/components/custom/buttons/TabButton"; +import JPCardTemplate from "@/components/dashboard/JobPostingCard/JPCardTemplate"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import Loading from "@/components/custom/Loading"; + + +export default function JobPostingCard({ className = "" }: IComponent) { + const [activeTab, setActiveTab] = useState ("Temporary"); + const [jobs, setJobs] = useState (); + const [loading, setLoading] = useState (true); + + useEffect(() => { + + const getData = async(activeTab : string) => { + if(activeTab === "Temporary"){ + api + .post("/jobs/professional/listing/1",{ + jobTypeId : "temporary" + }) + .then((res) => { + setJobs(res.data.jobs); + setLoading(false); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (activeTab === "Permanent"){ + api + .post("/jobs/professional/listing/1", { + jobTypeId : "fulltime" + }) + .then((res) => { + setJobs(res.data.jobs); + setLoading(false); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (activeTab === "Applied"){ + api + .post("/jobs/professional/listing/1",{ + jobTypeId : "applied" + }) + .then((res) => { + setJobs(res.data.jobs); + setLoading(false); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + } + getData(activeTab); + },[activeTab]); + + + return ( + + See All + + } + > + + ); +} diff --git a/components/forgot-password/ForgotPasswordForm.tsx b/components/forgot-password/ForgotPasswordForm.tsx new file mode 100644 index 0000000..3bd9941 --- /dev/null +++ b/components/forgot-password/ForgotPasswordForm.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { FormHTMLAttributes } from "react"; +import * as yup from "yup"; +import Input from "@/components/custom/Input"; +import { + VALIDATION_INVALID_EMAIL, + VALIDATION_REQUIRED_FIELD, +} from "@/utils/constants"; +import { useFormik } from "@/libraries/formik"; +import Button from "@/components/custom/buttons/Button"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; + +interface IProps extends FormHTMLAttributes++++ +setActiveTab("Temporary")} + > + Temporary + +setActiveTab("Permanent")} + > + Permanent + +setActiveTab("Applied")} + > + Applied + ++ {loading ?+: jobs?.length === 0 ? "No jobs to display" : + jobs?.map((job) => ( + + ))} + { + setLoading: (value: boolean | ((prev: boolean) => boolean)) => void; +} + +const validationSchema = yup.object().shape({ + email: yup + .string() + .email(VALIDATION_INVALID_EMAIL) + .required(VALIDATION_REQUIRED_FIELD), +}); + +export default function ForgotPasswordForm({ + className = "", + setLoading, +}: IProps) { + const router = useRouter(); + const { setEmailForOTP } = useAuth(); + + const formik = useFormik({ + initialValues: { + email: "", + }, + validationSchema, + onSubmit: (values) => { + setLoading(true); + api + .post("/send/forgot/password", values) + .then((res) => { + setLoading(false); + if (res.data.success) { + toast.info( + `A link for resetting password was sent to your mail inbox. ${res.data.code}` + ); + setEmailForOTP(values.email); + router.push("/otp-verify"); + } + }) + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + }, + }); + + return ( + + ); +} diff --git a/components/help/index.tsx b/components/help/index.tsx new file mode 100644 index 0000000..180f805 --- /dev/null +++ b/components/help/index.tsx @@ -0,0 +1,102 @@ +"use client"; + +import React, { useState } from "react"; +import CardTemplate from "../custom/CardTemplate"; +import { Typography, Collapse } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; + +interface FAQItem { + title: string; + desc: string; +} + +const DATA_ITEM: FAQItem = { + title: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor incididunt?", + desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ac auctor augue mauris augue neque gravida in. Blandit cursus risus at ultrices mi tempus imperdiet. Lacus luctus accumsan tortor posuere ac ut consequat. Interdum consectetur libero id faucibus nisl tincidunt eget nullam non. Cursus risus at ultrices mi. Quam elementum pulvinar etiam non. Blandit cursus risus at ultrices mi tempus imperdiet. Lacus luctus accumsan tortor posuere ac ut consequat. Interdum consect. ", +}; + +const DATA: FAQItem[] = Array(5).fill(DATA_ITEM); + +const HelpPage: React.FC = () => { + const [openIndex, setOpenIndex] = useState (null); + + const toggleOpen = (index: number) => + setOpenIndex((prevIndex) => (prevIndex === index ? null : index)); + + const isLastItem = (index: number) => index === DATA.length - 1; + + return ( + + + ); +}; + +export default HelpPage; diff --git a/components/job-posting/JobPostingPage.tsx b/components/job-posting/JobPostingPage.tsx new file mode 100644 index 0000000..7df6b3e --- /dev/null +++ b/components/job-posting/JobPostingPage.tsx @@ -0,0 +1,221 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { IComponent, IJob } from "@/utils/interfaces"; +import CardTemplate from "../custom/CardTemplate"; +import TabButton from "../custom/buttons/TabButton"; +import JobTabItem from "./JobTabItem"; +import { ButtonGroup, Button, Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import api from "@/utils/api"; +import { useUser } from "@/contexts/UserContext"; +import { getErrorMessage } from "@/utils/functions"; +import { toast } from "@/libraries/react-toastify"; +import Loading from "../custom/Loading"; + + +export default function JobPostingPage({ className = "" }: IComponent) { + const [activeTab, setActiveTab] = useState+++ FAQs + ++ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ac auctor + augue mauris augue neque gravida in. + ++ {DATA.map((item, index) => ( ++++ ))} +toggleOpen(index)} + > +++ {item.title} + ++ + ++ {item.desc} + ++++ Terms of Service + ++ Privacy Policy + +("Temporary"); + const [data, setData] = useState ([]); + const [loading, setLoading] = useState (true); + const {userData} = useUser(); + + const getData = async(activeTab : string) => { + if(activeTab === "Temporary"){ + api + .post("/jobs/professional/listing/1",{ + jobTypeId : "temporary" + }) + .then((res) => { + setLoading(false); + setData(res.data.jobs); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (activeTab === "Permanent"){ + api + .post("/jobs/professional/listing/1", { + jobTypeId : "fulltime" + }) + .then((res) => { + setLoading(false); + setData(res.data.jobs); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (activeTab === "Applied"){ + api + .post("/jobs/professional/listing/1",{ + jobTypeId : "applied" + }) + .then((res) => { + setLoading(false); + setData(res.data.jobs); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + }; + + useEffect(() => { + if(!userData || userData?.userType == 1) return ; + getData(activeTab); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeTab]); + + + return ( + + + ); +} diff --git a/components/job-posting/JobTabItem.tsx b/components/job-posting/JobTabItem.tsx new file mode 100644 index 0000000..767db0a --- /dev/null +++ b/components/job-posting/JobTabItem.tsx @@ -0,0 +1,429 @@ +"use client"; + +import React, { useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent, IJob } from "@/utils/interfaces"; +import { Avatar, Typography } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ellipsisString } from "@/utils/functions"; +import JobApplyDialog from "../dashboard/JobPostingCard/JobApplyDialog"; +import moment from "moment"; + +interface IProps extends IComponent { + job: IJob; + activeTab: string; +} + +export default function JobTabItem({ className = "", job, activeTab }: IProps) { + const [applyJob, setApplyJob] = useState++++ + {activeTab === "Temporary" ? ( +++ +setActiveTab("Temporary")} + > + Temporary + +setActiveTab("Permanent")} + > + Permanent + +setActiveTab("Applied")} + > + Applied + +++ ++ + {activeTab === "Temporary" || activeTab === "Permanent" ? ( +++ + + + + ) : ( ++ + + + )} ++ {data?.length} jobs meet + your Search + + ) : activeTab === "Permanent" ? ( ++ {data?.length} jobs meet + your Search + + ) : activeTab === "Applied" ? ( ++ {data?.length} jobs you + have applied + + ) : ( ++ Default Title + + )} + ++ {loading ?+: data?.map((job: IJob) => ( + + ))} + (false); + const handleApplyJob = () => { + setApplyJob(true); + }; + + const renderStars = (rate: any) => { + const stars = []; + for (let i = 1; i <= 5; i++) { + if (i <= rate) { + stars.push( + + ); + } else if (i - 0.5 === rate) { + stars.push( + + ); + } else { + stars.push( + + ); + } + } + return stars; + }; + + return ( + ++ ); +} diff --git a/components/layout/AuthLayout/LeftSection.tsx b/components/layout/AuthLayout/LeftSection.tsx new file mode 100644 index 0000000..5afcfdd --- /dev/null +++ b/components/layout/AuthLayout/LeftSection.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from "react"; +import { IComponent } from "@/utils/interfaces"; + +interface IProps extends IComponent { + children: ReactNode; +} + +export default function LeftSection({ children, className = "" }: IProps) { + return ( +++ {activeTab === "Temporary" ? ( ++ {job?.posterData?.avatar && ( +++ )} + +++++ {ellipsisString(`${job.title}`, 30)} + ++++ {`${job.title}`} + ++ {job?.posterData?.name} + ++ {job?.posterData && renderStars(parseFloat(job?.posterData.rate))} +++ {activeTab === "Applied" ? ( +++ {job?.applyStatus === "Application Rejected" ? ( ++ ) : ( + // : !job.isReadable ? ( + //+ ) : ( + + )} + + + {job?.applyStatus} + ++ //+ // ) ++ // Cannot apply due to no confirmed license in TX + // + //+ ++ )} + {activeTab === "Temporary" || activeTab === "Permanent" ? ( ++ {job.numOfAppliers} professionals have applied + + ) : ( + <>> + )} +++ ) : ( ++++ Date & Time + ++ {moment(job.postedAt).format("MM/DD/YYYY hh:mm a")} + ++++++ Break : + ++ {job.breakDuration} + ++++ Salary({job?.salary?.mode} Based): + ++ ${job?.salary?.min} - ${job?.salary?.max} + ++++ Payment Terms: + ++ {job.paymentTerms} + ++++ Payment Method: + ++ {job.paymentMethod} + +++ )} +++ ++ Practice Types + ++ {job.practiceTypes && + job.practiceTypes.map((item, index) => ( +++ {item?.name} + + ))} +++ ++ Description + ++++ {job.description ?? ""} + ++++ {job.description ? ellipsisString(job.description, 200) : ""} + + ++++ Pre Provided + ++ {job.preProvided && + job.preProvided.map((item, index) => ( +++ {item} + + ))} ++ Pro Types + ++ {job.proType && ( ++ ++ {job.proType} + + )} +++ +++ ++++ + {job?.experience || "None"} + +++ + {activeTab === "Temporary" ? ( ++++ + {job?.location || "None"} + +++ ) : ( ++++ + {job.distance || "None"} + +++ )} + ++++ + {job?.salary?.mode === null + ? "None" + : `$${job?.salary?.min} - $${job?.salary?.max}`} + ++++++ + {job.postedAt.slice(0, 10)} + ++ {activeTab === "Applied" ? ( +++ {job.status === "Application Rejected" ? ( ++ ) : !job.isReadable ? ( ++ ) : ( + + )} + + + {job.status} + +++ ) : ( ++ Cannot apply due to no confirmed license in TX + ++ ++ )} + {activeTab === "Temporary" || activeTab === "Permanent" ? ( ++ {job.numOfAppliers} professionals have applied + + ) : ( + <>> + )} ++ + + + + + {children} + + ); +} diff --git a/components/layout/AuthLayout/index.tsx b/components/layout/AuthLayout/index.tsx new file mode 100644 index 0000000..73130dd --- /dev/null +++ b/components/layout/AuthLayout/index.tsx @@ -0,0 +1,47 @@ +import { ReactNode, useEffect } from "react"; +// import { useRouter } from "next/navigation"; +// import { useLocalStorage } from "@/libraries/usehooks-ts"; +// import { +// L_STORAGE_AUTH_TOKEN, +// L_STORAGE_REFRESH_TOKEN, +// PATH_MAPPER, +// } from "@/utils/constants"; +// import api from "@/utils/api"; +// import { useAuth } from "@/contexts/AuthContext"; + +export default function AuthLayout({ children }: { children: ReactNode }) { + // const router = useRouter(); + // const [, setAuthTokenInStore] = useLocalStorage(L_STORAGE_AUTH_TOKEN, ""); + // const [, setRefreshTokenInStore] = useLocalStorage( + // L_STORAGE_REFRESH_TOKEN, + // "" + // ); + // const { authToken, setAuthToken, setRefreshToken } = useAuth(); + + // useEffect(() => { + // if (authToken) { + // api.post("/validate/token", { token: authToken }).then((res) => { + // const { valid } = res.data; + + // if (valid) { + // router.push(PATH_MAPPER.dashboard); + // } else { + // setAuthToken(""); + // setAuthTokenInStore(""); + // setRefreshToken(""); + // setRefreshTokenInStore(""); + // } + // }); + // } + // }, [authToken]); + + // if (authToken) { + // return <>>; + // } + + return ( ++ + ); +} diff --git a/components/layout/DashboardLayout/DPLayout/Header.tsx b/components/layout/DashboardLayout/DPLayout/Header.tsx new file mode 100644 index 0000000..39dc606 --- /dev/null +++ b/components/layout/DashboardLayout/DPLayout/Header.tsx @@ -0,0 +1,246 @@ +"use client"; + +import { AllHTMLAttributes, useCallback, useState } from "react"; +import { usePathname } from "next/navigation"; +import { ICON_MAPPER, PATHNAME_MAPPER } from "@/utils/constants"; +import { + Avatar, + Badge, + IconButton, + Typography, + Menu, + MenuHandler, + MenuList, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import RecommendAlert from "@/components/layout/DashboardLayout/RecommendAlert"; +import Msgmenu from "../MsgMenu"; +import NotiMenu from "@/components/layout/DashboardLayout/NotiMenu"; +import ProfileDialog from "../ProfileMenuDialogs/ProfileDialog"; +import BookingSettingsDialogue from "../ProfileMenuDialogs/BookingSettingDialog"; +import OfficesDialog from "../ProfileMenuDialogs/OfficeDialog"; +import EventsDialog from "../ProfileMenuDialogs/EventsDialg"; +import BillingInfoDialog from "../ProfileMenuDialogs/BillingInfoDialog"; +import NotificationDialog from "../ProfileMenuDialogs/NotificationDialog"; +import AccountSettingDialog from "../ProfileMenuDialogs/AccountSettingDialog/AccountSettingDialog"; +import LogOutDialogue from "../ProfileMenuDialogs/LogoutDialog/LogOutDialogue"; +import { useUser } from "@/contexts/UserContext"; +import EmptyAvatar from "@/components/custom/EmptyAvatar"; +import UserMenu from "../UserMenu"; + +interface IProps extends AllHTMLAttributes{children}+{ + setNotiDialogOpened: (value: boolean | ((prev: boolean) => boolean)) => void; +} + +export default function Header({ + className = "", + setNotiDialogOpened, +}: IProps) { + const pathname = usePathname(); + const { userData } = useUser(); + + //Menu// + const [msgMenuOpened, setMsgMenuOpened] = useState (false); + const [NotiMenuOpened, setNotiMenuOpened] = useState (false); + + const handleMsgMenuOpened = () => { + setMsgMenuOpened((prev) => !prev); + }; + const handleNotiMenuOpened = () => { + setNotiMenuOpened((prev) => !prev); + }; + + //Dialogs// + const [accountMenuOpened, setAccountMenuOpened] = useState (false); + const [profileDialogOpened, setProfileDialogOpened] = + useState (false); + const [bookingSettingsDialogOpened, setBookingSettingsDialogOpened] = + useState (false); + const [officesDialogOpened, setOfficesDialogOpened] = + useState (false); + const [eventsDialogOpened, setEventsDialogOpened] = useState (false); + const [billingInfoDialogOpened, setBillingInfoDialogOpened] = + useState (false); + const [notificationDialogOpened, setNotificationDialogOpened] = + useState (false); + const [accountSettingDialogOpened, setAccountSettingDialogOpened] = + useState (false); + const [logOutDialogOpened, setLogOutDialogOpened] = useState (false); + + const handleAccountMenuOpened = () => { + setAccountMenuOpened((prev) => !prev); + }; + + const handleMenuClick = useCallback((title: string) => { + switch (title) { + case "My Public Profile": + setProfileDialogOpened(true); + break; + case "Booking Settings": + setBookingSettingsDialogOpened(true); + break; + case "Offices": + setOfficesDialogOpened(true); + break; + // case "Events": + // setEventsDialogOpened(true); + // break; + case "Billing Info": + setBillingInfoDialogOpened(true); + break; + case "Notifications": + setNotificationDialogOpened(true); + break; + case "Account Settings": + setAccountSettingDialogOpened(true); + break; + case "Log out": + setLogOutDialogOpened(true); + break; + default: + break; + } + }, []); + + return ( + <> + + ++ {PATHNAME_MAPPER[pathname || ""] || ""} + + ++++ + + + + + + + + + {/* */} + + + + + > + ); +} diff --git a/components/layout/DashboardLayout/DPLayout/Navbar/NavItem.tsx b/components/layout/DashboardLayout/DPLayout/Navbar/NavItem.tsx new file mode 100644 index 0000000..0d587f0 --- /dev/null +++ b/components/layout/DashboardLayout/DPLayout/Navbar/NavItem.tsx @@ -0,0 +1,36 @@ +"use client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { IconButton, Tooltip } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { IComponent, INavItem } from "@/utils/interfaces"; + +interface IProps extends IComponent { + item: INavItem; +} + +export default function NavItem({ item }: IProps) { + const pathname = usePathname(); + return ( + + + + ); +} diff --git a/components/layout/DashboardLayout/DPLayout/Navbar/index.tsx b/components/layout/DashboardLayout/DPLayout/Navbar/index.tsx new file mode 100644 index 0000000..5627b1c --- /dev/null +++ b/components/layout/DashboardLayout/DPLayout/Navbar/index.tsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { IComponent } from "@/utils/interfaces"; +import { NAV_LINKS } from "@/utils/constants"; +import { IconButton, Tooltip } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import ReferDialog from "../../ReferDialog"; +import NavItem from "@/components/layout/DashboardLayout/DPLayout/Navbar/NavItem"; + +interface NavbarProps extends IComponent {} + +const Navbar: React.FC+ + ++ = ({ className = "" }: NavbarProps) => { + const [referDialogOpen, setReferDialogOpen] = useState (false); + const handleReferDialog = () => { + setReferDialogOpen((prev) => !prev); + }; + + return ( + + ); +}; + +export default Navbar; diff --git a/components/layout/DashboardLayout/DPLayout/index.tsx b/components/layout/DashboardLayout/DPLayout/index.tsx new file mode 100644 index 0000000..c8eb430 --- /dev/null +++ b/components/layout/DashboardLayout/DPLayout/index.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from "react"; +import Navbar from "@/components/layout/DashboardLayout/DPLayout/Navbar"; +import Header from "@/components/layout/DashboardLayout/DPLayout/Header"; +import useLoading from "@/hooks/useLoading"; +import Loading from "@/components/custom/Loading"; + +interface IProps { + children: ReactNode; + setNotiDialogOpened: (value: boolean | ((prev: boolean) => boolean)) => void; +} + +export default function DPLayout({ children, setNotiDialogOpened }: IProps) { + const { isLoading } = useLoading(); + + return ( + ++ ); +} diff --git a/components/layout/DashboardLayout/MBLayout/Header.tsx b/components/layout/DashboardLayout/MBLayout/Header.tsx new file mode 100644 index 0000000..ad7b4ba --- /dev/null +++ b/components/layout/DashboardLayout/MBLayout/Header.tsx @@ -0,0 +1,223 @@ +"use client"; +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { IComponent } from "@/utils/interfaces"; +import { + Avatar, + Badge, + IconButton, + Menu, + MenuHandler, + MenuItem, + MenuList, + Typography, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import RecommendAlert from "@/components/layout/DashboardLayout/RecommendAlert"; +import Navbar from "@/components/layout/DashboardLayout/MBLayout/Navbar"; +import { ICON_MAPPER, PATH_MAPPER } from "@/utils/constants"; +import { TEMP_MESSAGES, TEMP_NOTIFICATIONS } from "@/utils/tempData"; +import { ellipsisString } from "@/utils/functions"; +import NotiDialog from "@/components/layout/DashboardLayout/NotiDialog"; + +export default function Header({ className = "" }: IComponent) { + const [navbarOpened, setNavbarOpened] = useState++++ ++ +++ {isLoading ? : {children} } +(false); + const [messagesMenuOpened, setMessagesMenuOpened] = useState (false); + const [notisMenuOpened, setNotisMenuOpened] = useState (false); + const [notiDialogOpened, setNotiDialogOpened] = useState (false); + + const handleMessagesMenuOpened = () => { + setMessagesMenuOpened((prev) => !prev); + }; + + const handleNotisMenuOpened = () => { + setNotisMenuOpened((prev) => !prev); + }; + + return ( + <> + + ++ ++ ++ + + + + + + ++setNavbarOpened(true)} + > + ++ + + + > + ); +} diff --git a/components/layout/DashboardLayout/MBLayout/Navbar/NavItem.tsx b/components/layout/DashboardLayout/MBLayout/Navbar/NavItem.tsx new file mode 100644 index 0000000..0deaa36 --- /dev/null +++ b/components/layout/DashboardLayout/MBLayout/Navbar/NavItem.tsx @@ -0,0 +1,34 @@ +import { IComponent, INavItem } from "@/utils/interfaces"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + ListItem, + ListItemPrefix, + Typography, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; + +interface IProps extends IComponent { + item: INavItem; +} + +export default function NavItem({ item }: IProps) { + const pathname = usePathname(); + return ( + + + + + ); +} diff --git a/components/layout/DashboardLayout/MBLayout/Navbar/index.tsx b/components/layout/DashboardLayout/MBLayout/Navbar/index.tsx new file mode 100644 index 0000000..912504a --- /dev/null +++ b/components/layout/DashboardLayout/MBLayout/Navbar/index.tsx @@ -0,0 +1,56 @@ +import Link from "next/link"; +import Image from "next/image"; +import { IComponent } from "@/utils/interfaces"; +import { ICON_MAPPER, NAV_LINKS } from "@/utils/constants"; +import { Drawer, IconButton, List } from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import NavItem from "@/components/layout/DashboardLayout/MBLayout/Navbar/NavItem"; + +interface IProps extends IComponent { + opened: boolean; + setOpened: Function; +} + +export default function Navbar({ opened, setOpened }: IProps) { + const handleClose = () => { + setOpened(false); + }; + + return ( ++ ++ + {item.name} + ++ + ); +} diff --git a/components/layout/DashboardLayout/MBLayout/index.tsx b/components/layout/DashboardLayout/MBLayout/index.tsx new file mode 100644 index 0000000..598409e --- /dev/null +++ b/components/layout/DashboardLayout/MBLayout/index.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from "react"; +import Header from "@/components/layout/DashboardLayout/MBLayout/Header"; + +interface IProps { + children?: ReactNode; +} + +export default function MBLayout({ children }: IProps) { + return ( ++++ ++ ++ + + + ++ + {NAV_LINKS.primary.map((item) => ( +
+ ++ ))} + + {NAV_LINKS.secondary.map((item) => ( +
++ ))} + ++ ); +} diff --git a/components/layout/DashboardLayout/MsgMenu.tsx b/components/layout/DashboardLayout/MsgMenu.tsx new file mode 100644 index 0000000..25b2958 --- /dev/null +++ b/components/layout/DashboardLayout/MsgMenu.tsx @@ -0,0 +1,74 @@ +import { + Avatar, + List, + ListItem, + Typography, +} from "@/libraries/material-tailwind"; +import { TEMP_MESSAGES } from "@/utils/tempData"; +import { IMsg } from "@/utils/interfaces"; +import { ellipsisString } from "@/utils/functions"; +import { useEffect, useState } from "react"; +import api from "@/utils/api"; + +const MsgMenu: React.FC = () => { + const [messages, setMessages] = useState([]); + + const setData = async () => { + const res = await api.get("/user/get/latest/messages"); + setMessages(res?.data); + }; + + useEffect(() => { + setData(); + }, []); + + return ( ++ {children} ++
+ ); +}; + +export default MsgMenu; diff --git a/components/layout/DashboardLayout/NotiDialog.tsx b/components/layout/DashboardLayout/NotiDialog.tsx new file mode 100644 index 0000000..3d4c828 --- /dev/null +++ b/components/layout/DashboardLayout/NotiDialog.tsx @@ -0,0 +1,145 @@ +import { ICON_MAPPER } from "@/utils/constants"; +import { TEMP_NOTIFICATIONS } from "@/utils/tempData"; +import { IComponent, INotification } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { + Avatar, + Dialog, + DialogBody, + DialogHeader, + IconButton, + List, + ListItem, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { ellipsisString, getErrorMessage } from "@/utils/functions"; +import { useEffect, useState } from "react"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import Loading from "@/components/custom/Loading"; + +interface IProps extends IComponent { + notiDialogOpened: boolean; + setNotiDialogOpened: React.Dispatch++ {messages?.slice(0, 5).map((message: IMsg, i: number) => ( ++ Messages + ++ See All + ++ + ))} ++ + +++ {message?.senderData?.name} + ++ {ellipsisString(message.message, 55)} + +>; + size: DialogProps["size"]; +} + +const NotiDialog: React.FC = ({ + notiDialogOpened, + setNotiDialogOpened, + size, +}: IProps) => { + const handler = () => { + setNotiDialogOpened(!notiDialogOpened); + }; + + const [notify, setNotify] = useState<[]>([]); + const [loading, setLoading] = useState (true); + + useEffect(() => { + const getNotification = async() => { + await api + .get("/user/get/all/notifications/1") + .then((res) => { + setLoading(false); + setNotify(res.data.notifications); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + getNotification(); + }, []) + + return ( + + ); +}; + +export default NotiDialog; diff --git a/components/layout/DashboardLayout/NotiMenu.tsx b/components/layout/DashboardLayout/NotiMenu.tsx new file mode 100644 index 0000000..0e9991a --- /dev/null +++ b/components/layout/DashboardLayout/NotiMenu.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from "react"; +import { + Avatar, + List, + ListItem, + Typography, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { TEMP_NOTIFICATIONS } from "@/utils/tempData"; +import { ellipsisString, getErrorMessage } from "@/utils/functions"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { INotification } from "@/utils/interfaces"; +import Loading from "@/components/custom/Loading"; + +const NotiMenu = ({ + setNotiDialogOpened, +}: { + setNotiDialogOpened: (value: boolean | ((prev: boolean) => boolean)) => void; +}) => { + + const [notify, setNotify] = useState<[]>([]); + const [loading, setLoading] = useState (true); + + useEffect(() => { + const getNotification = async() => { + await api + .get("/user/get/top/notifications") + .then((res) => { + setLoading(false); + setNotify(res.data); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + getNotification(); + }) + + function timeconvert(timestamp : any){ + const formattedTime = new Date(timestamp).toLocaleTimeString([], { hour12: false }); + return formattedTime; + } + + return ( + +
+ ); +}; + +export default NotiMenu; diff --git a/components/layout/DashboardLayout/ProfileDialogue/StatusItem.tsx b/components/layout/DashboardLayout/ProfileDialogue/StatusItem.tsx new file mode 100644 index 0000000..3104b1e --- /dev/null +++ b/components/layout/DashboardLayout/ProfileDialogue/StatusItem.tsx @@ -0,0 +1,49 @@ +import { AllHTMLAttributes } from "react"; +import { Icon } from "@/libraries/iconify-react"; +import { IconButton } from "@/libraries/material-tailwind"; +import { ICON_MAPPER } from "@/utils/constants"; + +interface IProps extends AllHTMLAttributes++ { loading ? ( ++ Notification + ++ ) : ( + notify.map((noti : INotification, i) => ( + + + )))} + ++ {noti.imgSrc ? ( +++ ) : ( + ++ )} ++ +++ {noti.title} + ++++ {ellipsisString(noti.content, 25)} + ++ { + timeconvert(noti.receivedAt) + } + +setNotiDialogOpened(true)} + > + See All + +{ + label: string; + status: boolean; + onClick?: () => void; +} + +export default function StatusItem({ + label, + status, + onClick, + className = "", +}: IProps) { + return ( + ++ ); +} diff --git a/components/layout/DashboardLayout/ProfileDialogue/index.tsx b/components/layout/DashboardLayout/ProfileDialogue/index.tsx new file mode 100644 index 0000000..44b4141 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileDialogue/index.tsx @@ -0,0 +1,400 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { ICON_MAPPER, COLOR_MAPPER, MSG_SERVER_ERROR } from "@/utils/constants"; +import { IComponent, IUserStatus } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import Button from "@/components/custom/buttons/Button"; +import CardTemplate from "@/components/custom/CardTemplate"; +import TabButton from "@/components/custom/buttons/TabButton"; +import { + Avatar, + Card, + CardBody, + Dialog, + DialogBody, + DialogHeader, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { CircularProgressbarWithChildren } from "@/libraries/react-circular-progressbar"; +import BCalendar from "@/components/custom/calendars/BCalendar"; +import { useUser } from "@/contexts/UserContext"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import StatusItem from "./StatusItem"; +import EmptyAvatar from "@/components/custom/EmptyAvatar"; +import { useCalendar } from "@/contexts/CalendarContext"; +import { uuidv4 } from "@/libraries/uuid"; + +interface IProps extends IComponent { + profileDialogOpened: boolean; + setProfileDialogOpened: (value: boolean) => void; + size: DialogProps["size"]; +} + +let isFirstLoad = true; + +export default function ProfileDialog({ + profileDialogOpened, + setProfileDialogOpened, + size = "xl", +}: IProps) { + const { userData } = useUser(); + const { setEvents } = useCalendar(); + + const [activeTab, setActiveTab] = useState+ {status ? ( ++ ++ ) : ( + + )} + {label} + + {status ? ( + ++ ) : ( + + )} + ("Reviews Received"); + const [userStatus, setUserStatus] = useState (null); + + const statusCompletency = useMemo (() => { + if (!userStatus) return 0; + + const keys = Object.keys(userStatus); + const trueValues = Object.values(userStatus).filter((v) => v); + + return (trueValues.length / keys.length) * 100; + }, [userStatus]); + + const handler = () => { + setProfileDialogOpened(!profileDialogOpened); + }; + + useEffect(() => { + if (isFirstLoad) { + (async () => { + const { data: userStatusRes } = await api.get("/user/dashboard/status"); + if (userStatusRes.error) return toast.error(MSG_SERVER_ERROR); + setUserStatus(userStatusRes.data); + + const { data: calEventsRes } = await api.post("/calendar/events/list"); + if (calEventsRes.error) return toast.error(MSG_SERVER_ERROR); + setEvents( + calEventsRes.data.map((item: any) => ({ + id: item?.id || uuidv4(), + title: item?.title || "", + start: item?.start ? new Date(item?.start) : new Date(), + end: item?.end ? new Date(item?.end) : new Date(), + type: + item?.extendedProps?.type === "block" + ? "unavailable" + : "available", + blockId: item?.blockid, + })) + ); + })(); + } + isFirstLoad = false; + }, []); + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/AccountSettingDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/AccountSettingDialog.tsx new file mode 100644 index 0000000..9a43bdd --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/AccountSettingDialog.tsx @@ -0,0 +1,158 @@ +import { useState } from "react"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, + MTInput, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import ChangePasswordDialog from "./ChangePasswordDialog"; +import DeleteAccountDialog from "./DeleteAccountDialog"; + +interface IProps extends IComponent { + accountSettingDialogOpened: boolean; + setAccountSettingDialogOpened: Function; + size: DialogProps["size"]; +} + +export default function AccountSettingDialog({ + accountSettingDialogOpened, + setAccountSettingDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setAccountSettingDialogOpened(!accountSettingDialogOpened); + }; + + const [email, setEmail] = useState ("jamesmann7778@hotmail.com"); + const [phone, setPhone] = useState ("9372256346"); + + const handleEmailChange = (e: React.ChangeEvent ) => { + setEmail(e.target.value); + }; + + const handlePhoneChange = (e: React.ChangeEvent ) => { + setPhone(e.target.value); + }; + + const [passwordDialogOpened, setPasswordDialogOpened] = + useState (false); + const [deleteAccountDialogOpened, setDeleteAccountDialogOpened] = + useState (false); + + const handleChangePassDialog = () => { + setPasswordDialogOpened(true); + setAccountSettingDialogOpened(false); + }; + + const handleDeleteAccountDialog = () => { + setDeleteAccountDialogOpened(true); + setAccountSettingDialogOpened(false); + }; + + return ( + <> + + + + > + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/ChangePasswordDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/ChangePasswordDialog.tsx new file mode 100644 index 0000000..f16f5dc --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/ChangePasswordDialog.tsx @@ -0,0 +1,201 @@ +import { useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { + ICON_MAPPER, + VALIDATION_DISMATCH_PASSWORDS, + VALIDATION_REQUIRED_FIELD, +} from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import * as yup from "yup"; +import { useFormik } from "formik"; +import Input from "@/components/custom/Input"; + +type TPasswordType = "text" | "password"; + +const validationSchema = yup.object().shape({ + password: yup.string().required(VALIDATION_REQUIRED_FIELD), + confPassword: yup + .string() + .oneOf([yup.ref("password", undefined)], VALIDATION_DISMATCH_PASSWORDS) + .required(VALIDATION_REQUIRED_FIELD), +}); + +interface IProps extends IComponent { + passwordDialogOpened: boolean; + setPasswordDialogOpened: Function; + setAccountSettingDialogOpened: Function; + size: DialogProps["size"]; +} + +export default function ChangePasswordDialog({ + passwordDialogOpened, + setPasswordDialogOpened, + setAccountSettingDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setPasswordDialogOpened(!passwordDialogOpened); + setAccountSettingDialogOpened(true); + }; + + const formik = useFormik({ + initialValues: { + currentPass: "", + password: "", + confPassword: "", + }, + validationSchema, + onSubmit: () => {}, + }); + + const [currentPassType, setCurrentPassType] = + useState ("password"); + const [passwordType, setPasswordType] = useState ("password"); + const [confPasswordType, setConfPasswordType] = + useState ("password"); + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/DeleteAccountDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/DeleteAccountDialog.tsx new file mode 100644 index 0000000..cc19ffe --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/AccountSettingDialog/DeleteAccountDialog.tsx @@ -0,0 +1,82 @@ +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + Typography, + Button, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; + +interface IProps extends IComponent { + deleteAccountDialogOpened: boolean; + setDeleteAccountDialogOpened: Function; + setAccountSettingDialogOpened: Function; + size: DialogProps["size"]; +} + +export default function DeleteAccountDialog({ + deleteAccountDialogOpened, + setDeleteAccountDialogOpened, + setAccountSettingDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setDeleteAccountDialogOpened(!deleteAccountDialogOpened); + setAccountSettingDialogOpened(true); + }; + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/AddBillingCardDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/AddBillingCardDialog.tsx new file mode 100644 index 0000000..3fb745d --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/AddBillingCardDialog.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useState } from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { + Dialog, + DialogHeader, + DialogBody, + Typography, + MTInput, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { getErrorMessage } from "@/utils/functions"; + +interface IProps extends IComponent { + addBillingCardDialog: boolean; + setAddBillingCardDialog: Function; + handleSave: any; + size: DialogProps["size"]; +} + +export default function AddBillingCardDialog({ + addBillingCardDialog, + setAddBillingCardDialog, + handleSave, + size = "sm", +}: IProps) { + const handler = () => { + setAddBillingCardDialog(!addBillingCardDialog); + }; + + const [cardNumber, setCardNumber] = useState (""); + const [expiry, setExpiry] = useState (""); + const [pin, setPin] = useState (""); + + const handleChange = (e: any) => { + const { id, value } = e.target; + + if( id === "cardNum"){ + setCardNumber(value); + } else if ( id === "expDate"){ + setExpiry(value); + } else { + setPin(value); + } + }; + + const handleSaved = () => { + api + .post("/membership/save/credit/card", { + cardNumber: cardNumber, + expiry: expiry, + pin: pin + } ) + .then((res) => { + toast.success("Saved successfully."); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + + setCardNumber(""); + setExpiry(""); + setPin(""); + }; + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/index.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/index.tsx new file mode 100644 index 0000000..5e19062 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BillingInfoDialog/index.tsx @@ -0,0 +1,232 @@ +import { useState } from "react"; +import Image from "next/image"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Button, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { ICON_MAPPER } from "@/utils/constants"; +import AddBillingCardDialog from "./AddBillingCardDialog"; + +interface IProps extends IComponent { + billingInfoDialogOpened: boolean; + setBillingInfoDialogOpened: Function; + size: DialogProps["size"]; +} + +export default function BillingInfoDialog({ + billingInfoDialogOpened, + setBillingInfoDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setBillingInfoDialogOpened(!billingInfoDialogOpened); + }; + + const [addCardDialog, setAddCardDialog] = useState (false); + const [addBillingCardDialog, setAddBillingCardDialog] = + useState (false); + + const handleAdd = () => { + setAddBillingCardDialog(true); + }; + const handleSave = () => { + setAddCardDialog(true); + setAddBillingCardDialog(false); + }; + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/MoneyDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/MoneyDialog.tsx new file mode 100644 index 0000000..16380ff --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/MoneyDialog.tsx @@ -0,0 +1,164 @@ +import { useState, useEffect} from "react"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent, IBookingSetting } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, + MTInput, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; + +interface IProps extends IComponent { + moneyDialogOpened: boolean; + setMoneyDialogOpened: Function; + setBookingSettingsDialogOpened: Function; + originBookingSetting: IBookingSetting; + newBookingSetting: IBookingSetting; + setNewBookingSetting: Function; + size: DialogProps["size"]; +} + +export default function MoneyDialog({ + moneyDialogOpened, + setMoneyDialogOpened, + setBookingSettingsDialogOpened, + originBookingSetting, + newBookingSetting, + setNewBookingSetting, + size = "md", +}: IProps) { + const handler = () => { + setMoneyDialogOpened(!moneyDialogOpened); + setBookingSettingsDialogOpened(true); + }; + + const [value, setValue] = useState (0); + + const handleChange = (e: any) => { + setValue(e.target.value); + }; + + const handleHourlyRate = () => { + setNewBookingSetting({ + ...newBookingSetting, + hourlyRate: value + }); + setMoneyDialogOpened(!moneyDialogOpened); + setBookingSettingsDialogOpened(true); + } + + useEffect(() => { + setValue(originBookingSetting.hourlyRate); + }, []) + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TimeDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TimeDialog.tsx new file mode 100644 index 0000000..3b97188 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TimeDialog.tsx @@ -0,0 +1,173 @@ +import Button from "@/components/custom/buttons/Button"; +import { IComponent, IBookingSetting } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, + Slider, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { useState } from "react"; + + +interface IProps extends IComponent { + timeDialogOpened: boolean; + setTimeDialogOpened: Function; + setBookingSettingsDialogOpened: Function; + originBookingSetting: IBookingSetting; + newBookingSetting: IBookingSetting; + setNewBookingSetting: Function; + size: DialogProps["size"]; +} + +export default function TimeDialog({ + timeDialogOpened, + setTimeDialogOpened, + setBookingSettingsDialogOpened, + originBookingSetting, + newBookingSetting, + setNewBookingSetting, + size = "md", +}: IProps) { + const [tempMinimumHours, setTempMinimumHours] = useState (newBookingSetting.minHours); + + const handler = () => { + setTimeDialogOpened(!timeDialogOpened); + setBookingSettingsDialogOpened(true); + }; + + const travelRadiusChangeHandler = (v: React.ChangeEvent ) => { + setTempMinimumHours(parseInt(v.currentTarget.value)); + } + + const handleTimeRadius = () => { + setNewBookingSetting({ + ...newBookingSetting, + minHours: tempMinimumHours + }); + setTimeDialogOpened(!timeDialogOpened); + setBookingSettingsDialogOpened(true); + } + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TravelDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TravelDialog.tsx new file mode 100644 index 0000000..9a79472 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/TravelDialog.tsx @@ -0,0 +1,146 @@ +import Image from "next/image"; +import Button from "@/components/custom/buttons/Button"; +import { IComponent, IBookingSetting } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, + Slider, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { useState } from "react"; + +interface IProps extends IComponent { + travelDialogOpened: boolean; + setTravelDialogOpened: Function; + setBookingSettingsDialogOpened: Function; + originBookingSetting: IBookingSetting; + newBookingSetting: IBookingSetting; + setNewBookingSetting: Function; + size: DialogProps["size"]; +} + +export default function TravelDialog({ + travelDialogOpened, + setTravelDialogOpened, + setBookingSettingsDialogOpened, + originBookingSetting, + newBookingSetting, + setNewBookingSetting, + size = "md", +}: IProps) { + const [tempRadius, setTempRadius] = useState (newBookingSetting.travelRadius); + const handler = () => { + setTravelDialogOpened(!travelDialogOpened); + setBookingSettingsDialogOpened(true); + }; + + const travelRadiusChangeHandler = (v: React.ChangeEvent ) => { + setTempRadius(parseInt(v.currentTarget.value)); + } + + const handleNewTravelRadius = () => { + setNewBookingSetting({ + ...newBookingSetting, + travelRadius: tempRadius + }); + setTravelDialogOpened(!travelDialogOpened); + setBookingSettingsDialogOpened(true); + } + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/index.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/index.tsx new file mode 100644 index 0000000..ea80528 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/BookingSettingDialog/index.tsx @@ -0,0 +1,289 @@ +import { useState, useEffect } from "react"; +import TravelDialog from "./TravelDialog"; +import TimeDialog from "./TimeDialog"; +import MoneyDialog from "./MoneyDialog"; +import { IComponent, IBookingSetting } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Typography, +} from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { getErrorMessage } from "@/utils/functions"; + +interface IProps extends IComponent { + bookingSettingsDialogOpened: boolean; + setBookingSettingsDialogOpened: (value: boolean) => void; + size: DialogProps["size"]; +} + +export default function BookingSettingsDialogue({ + bookingSettingsDialogOpened, + setBookingSettingsDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setBookingSettingsDialogOpened(!bookingSettingsDialogOpened); + }; + + const [travelDialogOpened, setTravelDialogOpened] = useState (false); + const [timeDialogOpened, setTimeDialogOpened] = useState (false); + const [moneyDialogOpened, setMoneyDialogOpened] = useState (false); + const [originBookingSetting, setOriginBookingSetting] = + useState ({ + travelRadius: 0, + minHours: 0, + hourlyRate: 0, + }); + const [newBookingSetting, setNewBookingSetting] = useState ({ + travelRadius: 30, + minHours: 3, + hourlyRate: 10, + }); + + const travelDialogHandler = () => { + setTravelDialogOpened(true); + setBookingSettingsDialogOpened(false); + }; + + const timeDialogHandler = () => { + setTimeDialogOpened(true); + setBookingSettingsDialogOpened(false); + }; + + const moneyDialogHandler = () => { + setMoneyDialogOpened(true); + setBookingSettingsDialogOpened(false); + }; + + useEffect(() => { + api + .get("/user/get/business/settings") + .then((res) => { + const oBookingSetting = res.data; + if (!oBookingSetting) { + toast.error( + "Error! Something happened in getting origin booking setting from server." + ); + return; + } + setOriginBookingSetting(oBookingSetting); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }, []); + + const setNewBookingSettingHandler = (value: IBookingSetting) => { + setNewBookingSetting(value); + api + .post("/user/save/business/settings", value) + .then((res) => { + const isSuccess = res.data; + + if (isSuccess) { + toast.success("Success! Booking settings are changed."); + } else { + toast.warn("Booking settings change failed."); + } + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }; + + return ( + <> + + + + + > + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/EditProfileDialog/EditProfile.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/EditProfileDialog/EditProfile.tsx new file mode 100644 index 0000000..e633b9f --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/EditProfileDialog/EditProfile.tsx @@ -0,0 +1,312 @@ +import React, { useEffect, useRef, useMemo, useState, FormEvent, ChangeEvent } from "react"; +import { ICON_MAPPER, COLOR_MAPPER, MSG_SERVER_ERROR } from "@/utils/constants"; +import { Icon } from "@/libraries/iconify-react"; +import Button from "@/components/custom/buttons/Button"; +import { + Avatar, + DialogBody, + Typography, +} from "@/libraries/material-tailwind"; +import { useUser } from "@/contexts/UserContext"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import Input from "@/components/custom/Input"; +import yup from "@/libraries/yup"; +import { useFormik } from "@/libraries/formik"; +import { + VALIDATION_INVALID_EMAIL, + VALIDATION_REQUIRED_FIELD, +} from "@/utils/constants"; + + +function EditProfile() { + +// Get userData +const { userData } = useUser(); + +// Start of useState + + const [EditProfileDialog, setEditProfieDialog] = useState (false) + const profileInputFile = useRef (null); + const [profilePhoto, setProfilePhoto] = useState (null); + const [profilePhotoSrc, setProfilePhotoSrc] = useState ( + userData?.avatar ? userData.avatar : "" + ); + + + +// Start of functions + +const handleSubmit = (e: FormEvent ) => { + e.preventDefault(); + formik.handleSubmit(e); +}; + +const openProfilePhotoWindow = () => { + profileInputFile.current?.click(); +}; + +const selectProfilePhoto = (e: ChangeEvent ) => { + if (e.target.files && e.target.files[0]) { + setProfilePhoto(e.target.files[0]); + setProfilePhotoSrc(URL.createObjectURL(e.target.files[0])); + } +}; + + +// Formik implementation for edit profile dialog + +const validationSchema = yup.object().shape({ + firstname: yup + .string() + .required(VALIDATION_REQUIRED_FIELD), +}); + +const formik = useFormik({ + initialValues: { + firstName: userData?.firstName, + lastName: userData?.lastName, + address: "", + city: "", + zipCode: userData?.zipCode, + phone: userData?.phone, + jobMode: userData?.jobRole, + avatar: userData?.avatar, + }, + onSubmit: (values) => { + console.log(values) + console.log(profilePhoto) + + // POST METHOD FOR SUBMIT EDIT PROFILE + //setLoading(true); + /* + values.avatar = profilePhoto; + + const config = { + headers: { + "content-type": "multipart/form-data", + } + + api + .post("http://172.105.152.69/webapi/user/update/professional/profile/info", values, config) + .then((res) => { + setUserData((prev) => + prev + ? { + ...prev, + firstName: res.data.firstName, + lastName: res.data.lastName, + address: res.data.address, + city: res.data.city, + zipCode: res.data.zipCode, + phone: res.data.phone, + jobMode: res.data.jobMode, + avatar: res.data.profilePhoto, + } + : prev + ); + setLoading(false); + toast.success("Uploaded."); + .catch((err) => { + setLoading(false); + toast.error(getErrorMessage(err)); + }); + + }; + + */ + } +}); + + + return ( + + + ) +} + +export default EditProfile \ No newline at end of file diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/EventsDialg/index.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/EventsDialg/index.tsx new file mode 100644 index 0000000..017a2b6 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/EventsDialg/index.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from "react"; +import TabButton from "../../../../custom/buttons/TabButton"; +import { IComponent, IEvent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + ListItem, + Avatar, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import moment from "moment"; + +interface IProps extends IComponent { + eventsDialogOpened: boolean; + setEventsDialogOpened: Function; + size: DialogProps["size"]; +} + +export default function EventsDialog({ + eventsDialogOpened, + setEventsDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setEventsDialogOpened(!eventsDialogOpened); + }; + + const [activeTab, setActiveTab] = useState++ ++++ Edit Profile + ++ ++ ++ ++("New"); + const [eventList, setEventList] = useState (); + const [loading, setLoading] = useState (true); + + useEffect(() => { + api.post("/calendar/events/list") + .then(res => { + setLoading(false); + setEventList(res.data); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }) + }, []) + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/LogoutDialog/LogOutDialogue.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/LogoutDialog/LogOutDialogue.tsx new file mode 100644 index 0000000..00a0366 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/LogoutDialog/LogOutDialogue.tsx @@ -0,0 +1,78 @@ +import Button from "@/components/custom/buttons/Button"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { Dialog, DialogBody, Typography } from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import { + L_STORAGE_AUTH_TOKEN, + L_STORAGE_REFRESH_TOKEN, + PATH_MAPPER, +} from "@/utils/constants"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; + +interface IProps extends IComponent { + opened: boolean; + setOpened: Function; + size: DialogProps["size"]; +} + +export default function LogOutDialogue({ + opened, + setOpened, + size = "xs", +}: IProps) { + const router = useRouter(); + const { setAuthToken, setRefreshToken } = useAuth(); + + const handler = () => { + setOpened(!opened); + }; + + const handleLogout = () => { + localStorage.removeItem(L_STORAGE_AUTH_TOKEN); + localStorage.removeItem(L_STORAGE_REFRESH_TOKEN); + sessionStorage.removeItem(L_STORAGE_AUTH_TOKEN); + sessionStorage.removeItem(L_STORAGE_REFRESH_TOKEN); + setAuthToken(""); + setRefreshToken(""); + router.push(PATH_MAPPER.signin); + }; + + return ( + + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/NotificationDialog/index.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/NotificationDialog/index.tsx new file mode 100644 index 0000000..6b318fb --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/NotificationDialog/index.tsx @@ -0,0 +1,276 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import StatusBadge from "@/components/custom/StatusBadge"; +import { IComponent } from "@/utils/interfaces"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER, STATUS_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogBody, + Typography, + IconButton, + DialogProps, + Switch, +} from "@/libraries/material-tailwind"; +import { FastField } from "formik"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; + +interface IProps extends IComponent { + notificationDialog: boolean; + setNotificationDialog: React.Dispatch >; + size: DialogProps["size"]; +} + +interface INotiCategories { + id: string; + label: string; +} + +interface INotiSettings { + job: boolean; + booking: boolean; + message: boolean; + review: boolean; + scheduleReminder: boolean; +} + +const NOTI_CATEGORIES: INotiCategories[] = [ + { + id: "job", + label: "Job Applicants", + }, + { + id: "booking", + label: "Bookings", + }, + { + id: "message", + label: "Messages", + }, + { + id: "review", + label: "Reviews", + }, + { + id: "scheduleReminder", + label: "New talent", + }, +]; + +const NotificationsDialog: React.FC = ({ + notificationDialog, + setNotificationDialog, + size, +}: IProps) => { + const [notificationSettings, setNotificationSettings] = useState< + INotiSettings | any + >({ + job: false, + booking: false, + message: false, + review: false, + scheduleReminder: false, + }); + + const handler = () => { + setNotificationDialog(!notificationDialog); + }; + + const handleChange = (id: string) => { + const tempSettings: INotiSettings | any = notificationSettings; + tempSettings[id] = !notificationSettings[id]; + setNotificationSettings(tempSettings); + api + .post("user/notification/settings", notificationSettings) + .then((res) => { + toast.success("Notification setting changed!"); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }; + + useEffect(() => { + api + .get("user/notification/settings") + .then((res) => { + const notiSettings: INotiSettings = res.data; + if (notiSettings) { + setNotificationSettings(notiSettings); + } + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + }, []); + + return ( + <> + + > + ); +}; + +export default NotificationsDialog; diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/OfficeDialog/index.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/OfficeDialog/index.tsx new file mode 100644 index 0000000..b40edb7 --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/OfficeDialog/index.tsx @@ -0,0 +1,481 @@ +import React, { ChangeEvent, useEffect, useState } from "react"; +import { IComponent } from "@/utils/interfaces"; +import TabButton from "../../../../custom/buttons/TabButton"; +import Select from "../../../../custom/Select"; +import Button from "@/components/custom/buttons/Button"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import { + Dialog, + DialogHeader, + DialogBody, + IconButton, + Avatar, + Typography, +} from "@/libraries/material-tailwind"; +import type { DialogProps } from "@/libraries/material-tailwind"; +import api from "@/utils/api"; +import { toast } from "react-toastify"; +import { getErrorMessage } from "@/utils/functions"; + +interface IProps extends IComponent { + officesDialogOpened: boolean; + setOfficesDialogOpened: Function; + size: DialogProps["size"]; +} + +interface Office { + id: string; + avatar: string; + name: string; + travelRadius: string; + address: string; + isFavorite: boolean; + isBlocked: boolean; + addedToFav?: string; + addedToblock?: string; + skills?: string[]; + ppeProvided?: string[]; +} + +const SORT_BY = [ + { + id: 1, + label: "Name", + }, + { + id: 2, + label: "Distance", + }, + { + id: 3, + label: "Date Blocked", + }, +]; + +export default function OfficesDialog({ + officesDialogOpened, + setOfficesDialogOpened, + size = "lg", +}: IProps) { + const handler = () => { + setOfficesDialogOpened(!officesDialogOpened); + }; + + const [activeTab, setActiveTab] = useState ("All offices"); + const [selectedOffice, setSelectedOffice] = useState (null); + const [sortBy, setSortBy] = useState ("Sort by"); + + const handleSortBy = (e: ChangeEvent ) => { + setSortBy(e.target.value); + }; + + const [offices, setOffices] = useState ([]); + + const getFilteredOffices = () => { + if (activeTab === "Favorite Offices") { + return offices.filter((office) => office.isFavorite); + } else if (activeTab === "Blocked Offices") { + return offices.filter((office) => office.isBlocked); + } else { + return offices; + } + }; + + const filteredOffices = getFilteredOffices(); + + useEffect(() => { + setOffices([]); + + if (activeTab === "Favorite Offices") { + api + .post("/professional/offices/favorite") + .then((res) => { + setOffices(res.data.offices); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else if (activeTab === "Blocked Offices") { + api + .post("/professional/offices/blocked") + .then((res) => { + setOffices(res.data.offices); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } else { + api + .post("/professional/offices/all") + .then((res) => { + setOffices(res.data.offices); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + } + }, [activeTab]); + + const handleOfficeItem = (office: Office) => () => { + setSelectedOffice(office); + }; + const closeSelectedOfficeDialog = (e: any) => { + // e.stopPropagation(); + setSelectedOffice(null); + }; + + const handleBlockOffice = async (id: string, isBlocked: boolean) => { + await api + .post("/professional/office/set/blocked", { id, isBlocked: !isBlocked }) + .then((res) => { + if (res.data.success) { + toast.success("Success"); + } + }); + + setOffices((offices) => + offices.map((office) => { + if (office.id === id) { + return { + ...office, + isBlocked: !isBlocked, + }; + } + return office; + }) + ); + }; + + const handleFavoriteOffice = async (id: string, isFavorite: boolean) => { + await api + .post("/professional/office/set/favorites", { + id, + isFavorite: !isFavorite, + }) + .then((res) => { + if (res.data.success) { + toast.success("Success"); + } + }); + + setOffices((offices) => + offices.map((office) => { + if (office.id === id) { + return { + ...office, + isFavorite: !isFavorite, + }; + } + return office; + }) + ); + }; + + return ( + <> + + {selectedOffice && ( + + )} + > + ); +} diff --git a/components/layout/DashboardLayout/ProfileMenuDialogs/ProfileDialog/EditProfileDialog.tsx b/components/layout/DashboardLayout/ProfileMenuDialogs/ProfileDialog/EditProfileDialog.tsx new file mode 100644 index 0000000..f92c9dd --- /dev/null +++ b/components/layout/DashboardLayout/ProfileMenuDialogs/ProfileDialog/EditProfileDialog.tsx @@ -0,0 +1,409 @@ +import React, { useState } from "react"; +import { DialogHeader, DialogProps } from "@/libraries/material-tailwind"; +import { IComponent } from "@/utils/interfaces"; +import { + Dialog, + DialogBody, + IconButton, + Typography, + MTInput, +} from "@/libraries/material-tailwind"; +import { Icon } from "@/libraries/iconify-react"; +import { ICON_MAPPER } from "@/utils/constants"; +import Button from "@/components/custom/buttons/Button"; +import api from "@/utils/api"; +import { toast } from "@/libraries/react-toastify"; +import { getErrorMessage } from "@/utils/functions"; +import { Input } from "@material-tailwind/react"; +import Image from "next/image"; + +interface IProps extends IComponent { + addEditProfileDialog: boolean; + setAddEditProfileDialog: Function; + handleSave: any; + size: DialogProps["size"]; +} + +export default function EditProfileDialog({ + addEditProfileDialog, + setAddEditProfileDialog, + handleSave, + size = "lg", +}: IProps) { + const [fname, setFname] = useState (""); + const [lname, setLname] = useState (""); + const [address, setAddress] = useState (""); + const [city, setCity] = useState (""); + const [zipcode, setZipcode] = useState (""); + const [phone, setPhone] = useState (""); + const [selectPermanentJob, setSelectPermanentJob] = useState (false); + const [selectTempJob, setSelectTempJob] = useState (false); + const [photoID, setPhotoID] = useState (); + const [profilePhoto, setProfilePhoto] = useState (); + + const handler = () => { + setAddEditProfileDialog(!addEditProfileDialog); + }; + + const handleUpdate = () => { + api + .post("/user/update/professional/profile/info", { + firstName: fname, + lastName: lname, + address: address, + city: city, + zipCode: zipcode, + phone: phone, + jobMode: selectTempJob ? selectTempJob : selectPermanentJob, + avtar: File, + idImage: File, + }) + .then((res) => { + toast.success("Saved successfully."); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + }); + + setFname(""); + setLname(""); + setAddress(""); + setCity(""); + setZipcode(""); + setPhone(""); + setSelectPermanentJob(false); + setSelectTempJob(false); + setPhotoID(undefined); + setProfilePhoto(undefined); + }; + + const handleChange = (e: any) => { + const { id, value } = e.target; + + if (id === "fname") { + setFname(value); + } else if (id === "lname") { + setLname(value); + } else if (id === "address") { + setAddress(value); + } else if (id === "city") { + setCity(value); + } else if (id === "zipcode") { + setZipcode(value); + } else if (id === "phone") { + setPhone(value); + } else if (id === "profilephoto") { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onloadend = () => { + setProfilePhoto(reader.result); + }; + if (file) { + reader.readAsDataURL(file); + } + } else if (id === "photoid") { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onloadend = () => { + setPhotoID(reader.result); + }; + if (file) { + reader.readAsDataURL(file); + } + } + }; + + const handlePermanentJobClick = () => { + if (!selectPermanentJob) { + setSelectPermanentJob(true); + setSelectTempJob(false); + } + }; + + const handleTempJobClick = () => { + if (!selectTempJob) { + setSelectTempJob(true); + setSelectPermanentJob(false); + } + }; + + return ( + <> +