diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.hintrc b/.hintrc new file mode 100644 index 0000000..4d37573 --- /dev/null +++ b/.hintrc @@ -0,0 +1,14 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/forms": [ + "default", + { + "select-name": "off", + "label": "off" + } + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..34396f1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# dental_frontend diff --git a/app/bookings/layout.tsx b/app/bookings/layout.tsx new file mode 100644 index 0000000..4aa13e8 --- /dev/null +++ b/app/bookings/layout.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from "react"; + +export default function BookingLayout({ children }: { children: ReactNode }) { + return ( +
{children}
+ ); +} diff --git a/app/bookings/page.tsx b/app/bookings/page.tsx new file mode 100644 index 0000000..fcf18f6 --- /dev/null +++ b/app/bookings/page.tsx @@ -0,0 +1,9 @@ +import BookingCard from "@/components/bookings"; + +export default function Booking() { + return ( + <> + + + ); +} diff --git a/app/calendar/layout.tsx b/app/calendar/layout.tsx new file mode 100644 index 0000000..959965d --- /dev/null +++ b/app/calendar/layout.tsx @@ -0,0 +1,5 @@ +import { ILayoutProps } from "@/utils/interfaces"; + +export default function CalendarLayout({ children }: ILayoutProps) { + return
{children}
; +} diff --git a/app/calendar/page.tsx b/app/calendar/page.tsx new file mode 100644 index 0000000..d35b65a --- /dev/null +++ b/app/calendar/page.tsx @@ -0,0 +1,59 @@ +"use client"; +import { useEffect } from "react"; +import { Card, CardBody } from "@/libraries/material-tailwind"; +import { toast } from "@/libraries/react-toastify"; +import api from "@/utils/api"; +import { getErrorMessage } from "@/utils/functions"; +import useLoading from "@/hooks/useLoading"; +import { useCalendar } from "@/contexts/CalendarContext"; +import BCalendar from "@/components/custom/calendars/BCalendar"; +import { IEvent } from "@/utils/interfaces"; +import { useUser } from "@/contexts/UserContext"; + +let isFirstLoad = true; + +export default function CalendarPage() { + const { setIsLoading } = useLoading(); + const { setEvents } = useCalendar(); + const {userData} = useUser(); + + useEffect(() => { + if(!userData || userData?.userType == 1) return ; + if (isFirstLoad) { + setIsLoading(true); + api + .post("/calendar/events/list") + .then((res) => { + const processedData: Array = res.data.map( + (dataItem: { + id: string; + title: string; + doubleBooked: boolean; + date: string; + }) => ({ + id: dataItem.id, + title: dataItem.title, + doubleBooked: dataItem.doubleBooked, + start: new Date(`${dataItem.date}T00:00:00.00Z`), + end: new Date(`${dataItem.date}T23:59:59.00Z`), + }) + ); + setEvents(processedData); + setIsLoading(false); + }) + .catch((err) => { + toast.error(getErrorMessage(err)); + setIsLoading(false); + }); + } + isFirstLoad = false; + }, []); + + return ( + + + + + + ); +} diff --git a/app/create-password/layout.tsx b/app/create-password/layout.tsx new file mode 100644 index 0000000..0a409ce --- /dev/null +++ b/app/create-password/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +interface IProps { + children: ReactNode; +} + +export default function CreatePasswordLayout({ children }: IProps) { + return children; +} diff --git a/app/create-password/page.tsx b/app/create-password/page.tsx new file mode 100644 index 0000000..710b81e --- /dev/null +++ b/app/create-password/page.tsx @@ -0,0 +1,73 @@ +"use client"; +import Image from "next/image"; +import LeftSection from "@/components/layout/AuthLayout/LeftSection"; +import { Typography } from "@/libraries/material-tailwind"; +import CreatePasswordForm from "@/components/create-password/CreatePasswordForm"; +import Loading from "@/components/custom/Loading"; +import { useState } from "react"; + +export default function CreatePasswordPage() { + const [loading, setLoading] = useState(false); + + return ( + <> + +
+ + + Dental Jobs + +
+
+ + +
+ + Habitasse leo mi enim condimentum rhoncus. Sed non tortor gravida . + +
+
+ {loading ? ( + + ) : ( +
+ + Create New Password + + +
+ )} +
+ + ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx new file mode 100644 index 0000000..4bdc668 --- /dev/null +++ b/app/dashboard/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +export default function DashboardLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..8a14c4f --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,28 @@ +"use client"; +import AvatarCard from "@/components/dashboard/AvatarCard"; +import BookingsCard from "@/components/dashboard/BookingsCard"; +import CalendarCard from "@/components/dashboard/CalendarCard"; +import CompleteProfileCard from "@/components/dashboard/CompleteProfileCard"; +import JobPostingCard from "@/components/dashboard/JobPostingCard"; +import { useUser } from "@/contexts/UserContext"; +import { useEffect } from "react"; + +export default function DashboardPage() { + const { userData } = useUser(); + + useEffect(() => { + if (!userData || userData?.userType == 1) return; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + + + + + + ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ 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 ? ( + + ) : ( +
+ + Forgot Password? + + +
+ )} +
+ + ); +} 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
{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 ? ( + + ) : ( +
+ + OTP Verification + + +
+ )} +
+ + ); +} 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 ( + + + + + + {isAuthLayout(pathname) ? ( + {children} + ) : ( + {children} + )} + + + + + + ); +} 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
{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 ? ( + + ) : ( + <> +
+ + +
+
+ + Login + + + + Haven't created your free account? +
+ + Sign up Dental Office + {" "} + or{" "} + + Dental Professional + +
+
+ + )} +
+ + ); +} 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(""); + + 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 . + + +
+
+ +
+
+ + +
+
+ + 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/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 ( +
+
+ + 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/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) => ( + +
+
+ + {b.label} + + + + + + + {/* {b.status} */} +
+
+ + Date and Time + + + {b.bookedDateTime.slice(0, 10)}-{b.bookedDateTime.slice(11, 17)} + +
+
+ + Hourly Rate + + + {b.hourlyRate} + +
+
+
+ ))} + + ); +} 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("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 ( + + + + + ) : 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 + +
+
+ +
+
+ +
+
+
+ ); +} 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 { + 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 ( +
+
+ + setPasswordType((prev) => + prev === "text" ? "password" : "text" + ) + } + variant="text" + > + + + } + onChange={formik.handleChange} + value={formik.values.password} + error={formik.touched.password && formik.errors.password} + /> + + setConfPasswordType((prev) => + prev === "text" ? "password" : "text" + ) + } + variant="text" + > + + + } + onChange={formik.handleChange} + value={formik.values.confPassword} + error={formik.touched.confPassword && formik.errors.confPassword} + /> +
+ + +
+ ); +} 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 ( + + +
+ + {title} + + {title === "Hi, James Mann!" ? ( + + Public Profile + + ) : ( + "" + )} + {actions} +
+ {children} +
+
+ ); +} 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("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 && ( + + )} + +
+ {startAdornment ? ( +
+ {startAdornment} + setFocused(true)} + onBlur={() => setFocused(false)} + /> +
+ ) : ( + setFocused(true)} + onBlur={() => setFocused(false)} + /> + )} + + {!!endAdornment && ( +
setShowPassword(true)} + onMouseUp={() => setShowPassword(false)} + > + {endAdornment} +
+ )} +
+ + {!!error && ( + + {error} + + )} +
+ ); +} 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 ( +
+ +
+ ); +} 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 ( +
+ + + + + {monthYear} + + + + +
+ ); +} 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 { + 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 ( + + +
+ + Exception to availability + + + Date: {moment(date).format("ll")} + +
+ + {availability === "custom" && ( +
+
+ From + +
+ + {fromDateWeeks.map((dw) => ( + + setFromDate( + new Date( + fromDate.getFullYear(), + fromDate.getMonth(), + dw.date + ) + ) + } + > +
+ {dw.date} + {dw.weekDay} +
+
+ ))} +
+ +
+ To + +
+ + {toDateWeeks.map((dw) => ( + + setToDate( + new Date(toDate.getFullYear(), toDate.getMonth(), dw.date) + ) + } + > +
+ {dw.date} + {dw.weekDay} +
+
+ ))} +
+
+ )} + +
+ + Set to unavailable + + } + className="border-primary checked:border-primary checked:before:border-primary before:bg-primary checked:before:bg-primary text-primary" + iconProps={{ + className: "text-primary", + }} + checked={availability === "unavailable"} + onChange={() => setAvailibility("unavailable")} + /> + +
+ +
+ + Set to available at these items + + +
+ + From: + + } + className="border-primary checked:border-primary checked:before:border-primary before:bg-primary checked:before:bg-primary text-primary" + iconProps={{ + className: "text-primary", + }} + checked={availability === "custom"} + onChange={() => setAvailibility("custom")} + /> + + + To: + + +
+
+ +
+ + + Use availability as set in{" "} + + Typical Availability + + + } + className="border-primary checked:border-primary checked:before:border-primary before:bg-primary checked:before:bg-primary text-primary" + iconProps={{ + className: "text-primary", + }} + checked={availability === "typical"} + onChange={() => setAvailibility("typical")} + /> +
+
+ + +
+
+
+ ); +} 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 ( + + +
+ +
+ + + Are you sure want to delete? +
This exception from{" "} + + {" "} + {moment(event.start).format("MM/DD/YYYY hh:mm a")} to{" "} + {moment(event.end).format("MM/DD/YYYY hh:mm a")} + + . +
+ +
+ + +
+
+
+ ); +} 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 ( +
+

{title}

+
+ ); +} 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 ( +
+ + {label} + + + {label[0]} + +
+ ); +} 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 { + 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)} + > + + + + + +
+ + + {date.getDate() === currentDate.getDate() + ? "Today" + : moment(date).format("l")} + +
+
+ + + +
+ + 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 ? ( +
+
Calendar
+
+ gotoNeighborMonth(false)} + > + + +

{monthYear}

+ gotoNeighborMonth(true)} + > + + +
+
+ ) : ( +
+ gotoNeighborMonth(false)} + > + + +

{monthYear}

+ gotoNeighborMonth(true)} + > + + +
+ )} + +
+ SHORT_WEEKDAYS[date.getDay()]} + calendarType="gregory" + value={date} + /> + {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(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 ( + +
+
+
+ + {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 + +
+ +
+
+
+
+
+ ); +} 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("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 + + } + > +
+
+ setActiveTab("Today")} + > + Today + + setActiveTab("Future")} + > + Future + + setActiveTab("Past")} + > + Past + +
+ + + {filterBookings()?.map((b, i) => ( + +
+ + {b.title} + + + {moment(b.bookedAt).format("llll")} + +
+ + {b.status} +
+ ))} +
+
+
+ ); +} 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) { + 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 ( + + +
+ + + +
+
+
+ + Add Your License Number + + +
+
+ + License/Certification + + + Our promise to dental jobs is presenting verified professionals. + (To be equal, we verify dental offices, too). + + + Are you licensed or certified? + + + If licensed, give me your license number. + + +
+ +
+
+ + +
+
+
+
+
+ ); +} 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 ( + + {loading ? ( + + ) : ( + +
+ + + +
+
+
+ + Complete A Background Check + +
+
+ + Increase your marketability with a background check badge + +
+
+ + James Mann + +
+ +
+ + +

Unlock the background check badge for your profile

+
+ + +

Increase the number of offices you are eligible to work for

+
+ + +

Includes:

+
+ +
+ + +

SSN Trace

+
+ + +

National Criminal Search

+
+ +
+ {LIST_CRIMINAL_SEARCH.map((e) =>( + +
  • {e.label}
  • +
    + ) + )} +
    + + + +

    Sex Offender Search

    +
    + + +

    Global Watchlist Search

    +
    + + +

    Terrorist Watchlist Search

    +
    + +
    + + +

    CLEAR checks are valid for 1 year from the date of issue

    +
    + +
    + + $15.00 + +
    +
    + + By continuing, you agree to a background check and are purchasing the Background Check Badge for your Cloud + Dentistry profile. The Background Check Badge will only be displayed if your background check is deemed CLEAR + according to the rules above. No refunds are issued for failed or incomplete background checks. + +
    +
    +
    +
    + + Payment Details + + + Card Number + + +
    +
    + + Expiry Date + + +
    +
    + + PIN + + +
    +
    +
    + +
    +
    +
    + )} +
    + ) +} + 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 ( + + {loading ? ( + + ) : ( + +
    + + + +
    +
    +
    + + Background Description + + +
    +
    + + Share what makes you great! Include your years of experience, + why you love what you do, your skills and the practice + management systems you use, and everything else that makes you + great at your job. The more descriptive, the better - this is + what offices see. + +