mearge dev

This commit is contained in:
2025-09-23 11:24:11 +03:30
375 changed files with 11529 additions and 37562 deletions
+3
View File
@@ -34,3 +34,6 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.idx/dev.nix
.vscode/settings.json
.env
+48
View File
@@ -0,0 +1,48 @@
# Dockerfile for Next.js 14 - Optimized for Production
# Use the official Node.js image with Alpine Linux for a smaller footprint
FROM node:22-alpine AS base
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json (or yarn.lock)
COPY package*.json ./
# Install dependencies - leveraging Docker cache
RUN npm i --force
# Copy the rest of the application code
COPY . .
# Build the Next.js application
RUN npm run build
# Stage 2: Production image - smaller and leaner
FROM node:22-alpine AS runner
# Set the working directory
WORKDIR /app
# Set environment variables
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# Add a non-root user for security
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Copy only the necessary files from the builder stage
COPY --from=base /app/next.config.js ./
COPY --from=base /app/public ./public
COPY --from=base --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=base --chown=nextjs:nodejs /app/.next/static ./.next/static
# Change ownership of all copied files to the non-root user
USER nextjs
# Expose the port Next.js listens on
EXPOSE 3000
# Command to start the Next.js server in production mode
CMD ["node", "server.js"]
+10 -2
View File
@@ -29,8 +29,6 @@ npm install
## 🛠 Available Scripts ## 🛠 Available Scripts
In the project directory, you can run:
```bash ```bash
npm run dev npm run dev
``` ```
@@ -55,6 +53,16 @@ npm run lint
Checks the code for linting issues. Checks the code for linting issues.
## ⚙️ Environment Variables
Create a `.env` file in the root directory with the following variables:
```
REACT_APP_API_URL=https://back-dev.clinic-pro.ir
REACT_APP_CLIENT_ID=NWuujdYHjtAtrhrk_zoWB3w0kyf56wRCOaY36MIqvbs
REACT_APP_CLIENT_SECRET=trhrk_zoWB3w0kyf
```
## 🌐 Features ## 🌐 Features
- ✅ Responsive design using MUI and Tailwind - ✅ Responsive design using MUI and Tailwind
+2 -2
View File
@@ -1,9 +1,9 @@
import AboutUsPage from "@/components/aboutUs"; import AboutUsPage from "@/components/aboutUs";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
function AboutUs() { function AboutUs() {
return ( return (
<Layout title="درباره ما" name="about-us"> <Layout name="/about-us">
<AboutUsPage /> <AboutUsPage />
</Layout> </Layout>
); );
+21
View File
@@ -0,0 +1,21 @@
import AppointmentPage from "@/components/appointment";
import { getStateInfo } from "@/lib/getStateInfo";
import axios from "axios";
async function Appointment({ params: { slug } }) {
const { matchedCity } = getStateInfo();
const API_URL = process.env.NEXT_PUBLIC_API_URL;
let apt = null;
try {
const response = await axios.get(`${API_URL}/api/v1/appointment-slots`);
apt = response.data.data;
} catch (error) {
// console.error("problem with req:", error.message);
}
return <AppointmentPage slug={slug} matchedCity={matchedCity} />;
}
export default Appointment;
+9 -6
View File
@@ -1,13 +1,16 @@
import BlogPage from "@/components/blog"; import BlogPage from "@/components/blog";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import articles from "@/data/articles.json"; import { fetchReq } from "@/lib/req";
function Blog({ params: { slug } }) { async function Blog({ params: { slug } }) {
const article = articles.find((item) => item.id === +slug); const API_URL = process.env.NEXT_PUBLIC_API_URL;
const blog = await fetchReq(`${API_URL}/api/v1/blog/${slug}`);
const blogs = await fetchReq(`${API_URL}/api/v1/blogs`);
return ( return (
<Layout title="وبلاگ" name="blog"> <Layout>
<BlogPage article={article} articles={articles} /> <BlogPage blog={blog} blogs={blogs.blogs} />
</Layout> </Layout>
); );
} }
+10 -6
View File
@@ -1,12 +1,16 @@
import BlogsPage from "@/components/blogs"; import BlogsPage from "@/components/blogs";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import { fetchReq } from "@/lib/req";
export default async function Blogs() {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const response = await fetchReq(`${API_URL}/api/v1/blogs`);
const blogs = response.blogs;
function Blogs() {
return ( return (
<Layout title="وبلاگ ها" name="blogs"> <Layout name="/blogs">
<BlogsPage /> <BlogsPage blogs={blogs} />
</Layout> </Layout>
); );
} }
export default Blogs;
-71
View File
@@ -1,71 +0,0 @@
"use client";
import { useState } from "react";
import Layout from "@/components/layout";
import doctors from "@/data/doctors.json";
import Date from "@/components/booking/date";
import BookingPage from "@/components/booking";
import RegisterPage from "@/components/register";
import Paying from "@/components/booking/paying";
import Detail from "@/components/booking/detail";
import FailedPay from "@/components/booking/failedPay";
import LogInPage from "@/components/register/LogInPage";
import SuccessPay from "@/components/booking/successPay";
import SetCodePage from "@/components/register/SetCodePage";
function Booking({ params: { slug } }) {
const doctor = doctors.find((item) => item.id === +slug);
const [step, setStep] = useState(0);
const [isForAnother, setIsForAnother] = useState(false);
const [data, setData] = useState({
phone: { value: "09121056987", isEdit: true },
codemeli: { value: "1741025645", isEdit: true },
name: { value: "ساغر صابری نژاد", isEdit: true },
});
const elements = [
<Date doctor={doctor} setStep={setStep} />,
<LogInPage setIsSendMsg={false} setStep={setStep} />,
<SetCodePage setIsSendMsg={false} link={false} setStep={setStep} />,
<Detail
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
setStep={setStep}
data={data}
setData={setData}
/>,
<Paying setStep={setStep} />,
<SuccessPay setStep={setStep} />,
<FailedPay setStep={setStep} />,
];
return (
<>
{step === 1 || step === 2 ? (
<RegisterPage>{elements[step]}</RegisterPage>
) : (
<Layout
title="رزرو"
name="booking"
disableFooter={step > 2}
paddingBottom="pb-[80px]"
>
<BookingPage
disableSide={step === 5 || step === 6}
isFirst={step === 0}
setStep={setStep}
doctor={doctor}
step={step}
>
{elements[step]}
</BookingPage>
</Layout>
)}
</>
);
}
export default Booking;
+17 -7
View File
@@ -1,16 +1,26 @@
import ClinicPage from "@/components/clinic"; import ClinicPage from "@/components/clinic";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import clinics from "@/data/clinics.json"; import { fetchReq } from "@/lib/req";
import doctors from "@/data/doctors.json";
function Blog({ params: { slug } }) { async function Clinic({ params: { slug } }) {
const clinic = clinics.find((item) => item.id === +slug); const API_URL = process.env.NEXT_PUBLIC_API_URL;
const reqClinics = await fetchReq(
`${API_URL}/api/v1/clinic/${slug}`
);
const reqDoctors = await fetchReq(
`${API_URL}/api/v1/doctors`
);
const clinic = reqClinics;
const doctors = reqDoctors?.data || [];
return ( return (
<Layout title="کلینیک" name="clinic"> <Layout>
<ClinicPage data={clinic} doctors={doctors} /> <ClinicPage data={clinic} doctors={doctors} />
</Layout> </Layout>
); );
} }
export default Blog; export default Clinic;
+16 -6
View File
@@ -1,12 +1,22 @@
// app/clinics/page.js
import ClinicsPage from "@/components/clinics"; import ClinicsPage from "@/components/clinics";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import { fetchReq } from "@/lib/req";
import { getStateInfo } from "@/lib/getStateInfo";
export default async function Clinics() {
const { matchedCity, matchedState } = getStateInfo();
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const clinics = await fetchReq(`${API_URL}/api/v1/clinics`);
function Clinics() {
return ( return (
<Layout title="کلینیک ها" name="clinics"> <Layout name="/clinics">
<ClinicsPage /> <ClinicsPage
clinics={clinics && clinics.data}
matchedCity={matchedCity}
matchedState={matchedState}
/>
</Layout> </Layout>
); );
} }
export default Clinics;
+2 -5
View File
@@ -1,11 +1,8 @@
function AnimationTextHead({ name, text, children }) { function AnimationTextHead({ text, children }) {
return ( return (
<div className="flex justify-center items-start relative w-fit"> <div className="flex justify-center items-start relative w-fit">
<div className="overflow-hidden mb-[4px] md:mb-[5px] lg:mb-[6px]"> <div className="overflow-hidden mb-[4px] md:mb-[5px] lg:mb-[6px]">
<p <p className="text-[#3B3B3B] text-nowrap overflow-hidden text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
data-aos={name}
className="text-[#3B3B3B] text-nowrap overflow-hidden text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold"
>
{text} {text}
</p> </p>
</div> </div>
+82
View File
@@ -0,0 +1,82 @@
import { useState } from "react";
import ProfilePA from "@/components/icons/ProfilePA";
import LogoutP from "@/components/icons/LogoutP";
import CardDA from "@/components/icons/CardDA";
import { Button, Popover } from "@mui/material";
import Link from "next/link";
import ModalLogout from "./ModalLogout";
function DetailProfile({ anchorEl, setAnchorEl }) {
const [modal, setModal] = useState(false);
const handleClose = () => setAnchorEl(null);
const closeModal = () => setModal(false);
const openModal = () => setModal(true);
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<>
<ModalLogout open={modal} handleClose={closeModal} />
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<div className="flex pt-[11px] pb-[14px] flex-col items-start gap-[8px] min-w-[185px]">
<Link href="/dashboard" className="w-full">
<Button
fullWidth
variant="text"
className="!text-[#7E7E7E] !text-[14px] !font-medium !flex !justify-start !p-[8px] !gap-[5px]"
>
<ProfilePA />
پروفایل من
</Button>
</Link>
<Link
href={{
pathname: "/dashboard",
query: {
sidebar: 2,
},
}}
className="w-full"
>
<Button
fullWidth
variant="text"
className="!text-[#7E7E7E] !text-[14px] !font-medium !flex !justify-start !p-[8px] !gap-[5px]"
>
<CardDA />
تراکنش های من
</Button>
</Link>
<Button
fullWidth
color="error"
variant="contained"
onClick={openModal}
sx={{
"&.MuiButtonBase-root": {
paddingTop: "8px !important",
paddingBottom: "8px !important",
},
}}
className="!flex !w-[calc(100%-16px)] !mx-auto !text-[#FAFAFA] !text-[14px] !font-medium !items-center !justify-center !gap-[5px]"
>
<LogoutP />
خروج
</Button>
</div>
</Popover>
</>
);
}
export default DetailProfile;
+50 -26
View File
@@ -1,5 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import { Button, Skeleton } from "@mui/material"; import { Button } from "@mui/material";
// Icons // Icons
import ArrowLeftD from "@/components/icons/ArrowLeftD"; import ArrowLeftD from "@/components/icons/ArrowLeftD";
@@ -10,44 +10,65 @@ import Link from "next/link";
import CustomLoading from "./loading/Custom"; import CustomLoading from "./loading/Custom";
import TextLoading from "./loading/Text"; import TextLoading from "./loading/Text";
import CircularLoading from "./loading/Circular"; import CircularLoading from "./loading/Circular";
import moment from "moment-jalaali";
import { useState } from "react";
moment.loadPersian({ dialect: "persian-modern" });
function ItemDoctor({ doctor, doctors, setDoctors }) {
const [loading, setLoading] = useState(false);
const handleLoading = () => {
setDoctors((prevDoctors) =>
prevDoctors.map((item) =>
item.id === doctor.id
? { ...item, loading: true }
: { ...item, loading: false }
)
);
};
function ItemDoctor({ doctor, loading }) {
return ( return (
<li className="p-4 relative rounded-lg bg-[#FFF] border border-[#EFEFEF]"> <li className="p-4 relative rounded-lg bg-[#FFF] border border-[#EFEFEF]">
{/* Detail Doctor */} {/* Detail Doctor */}
<div className="flex items-center justify-start gap-2.5"> <div className="flex items-center justify-start gap-2.5">
<CircularLoading loading={loading} width={66} height={66}> <CircularLoading width={66} height={66}>
<Image <Image
width={72} width={72}
height={72} height={72}
src={doctor.img}
alt="image-doctor" alt="image-doctor"
className="w-[64px] md:w-[68px] lg:w-[72px] h-[64px] md:h-[68px] lg:h-[72px]" src={doctor?.img[0]?.url}
className="object-contain w-[64px] md:w-[68px] lg:w-[72px] h-[64px] md:h-[68px] lg:h-[72px]"
/> />
</CircularLoading> </CircularLoading>
<div className="flex flex-col items-start justify-center gap-2"> <div className="flex flex-col items-start justify-center gap-2">
<TextLoading width={90} height={15} loading={loading}> <TextLoading width={90} height={15}>
<p className="text-[#3B3B3B] text-[14px] font-bold"> <Link href={`/doctor/${doctor?.uuid}`}>
{doctor.name} <p className="text-[#3B3B3B] text-[14px] font-bold">
</p> {doctor?.name}
</p>
</Link>
</TextLoading> </TextLoading>
<TextLoading width={120} height={15} loading={loading}> <TextLoading width={120} height={15}>
<p className="text-[#616161] text-[14px] font-normal"> <p className="text-[#616161] text-[14px] font-normal">
تخصص: {doctor.expertise} تخصص:
{doctor?.specialties?.map(
(item, idx) =>
`${item.name} ${doctor.specialties.length === idx + 1 ? "" : "|"} `
)}
</p> </p>
</TextLoading> </TextLoading>
<CustomLoading width={100} height={25} loading={loading}> <CustomLoading width={100} height={25}>
<div className="flex rounded items-center justify-start gap-2"> <div className="flex rounded items-center justify-start gap-2">
<div className="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5"> <div className="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5">
<LikeD /> <LikeD />
<p className="text-[#616161] text-[12px] font-normal"> <p className="text-[#616161] text-[12px] font-normal">
{doctor.satisfaction}% {doctor?.satisfaction}%
</p> </p>
</div> </div>
<div className="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5"> <div className="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5">
<PointD /> <PointD />
<p className="text-[#616161] text-[12px] font-normal"> <p className="text-[#616161] text-[12px] font-normal">
{doctor.point} {doctor?.point}
</p> </p>
</div> </div>
</div> </div>
@@ -58,36 +79,39 @@ function ItemDoctor({ doctor, loading }) {
{/* Hours of work */} {/* Hours of work */}
<div className="flex my-4 items-center justify-start gap-0.5"> <div className="flex my-4 items-center justify-start gap-0.5">
<ClockD /> <ClockD />
<TextLoading width={150} height={15} loading={loading}> <TextLoading width={150} height={15}>
<p className="text-[#7E7E7E] text-[12px] font-normal"> <p className="text-[#7E7E7E] text-[12px] font-normal">
ساعت کاری: {doctor.hours_of_work} ساعت کاری: {doctor?.hours_of_work}
</p> </p>
</TextLoading> </TextLoading>
</div> </div>
<CustomLoading loading={loading} width={150} height={20}> <CustomLoading width={150} height={20}>
<p <p
className={`text-[12px] font-medium rounded w-fit p-1.5 ml-[52px] className={`text-[12px] font-medium rounded w-fit p-1.5 ml-[52px]
${ ${
doctor.free_turn Number(doctor?.active)
? "bg-[rgba(5,_186,_88,_0.04)] text-[#05BA58]" ? "bg-[rgba(5,_186,_88,_0.04)] text-[#05BA58]"
: "bg-[rgba(211,_47,_47,_0.04)] text-[#D32F2F]" : "bg-[rgba(211,_47,_47,_0.04)] text-[#D32F2F]"
}`} }`}
> >
{doctor.free_turn {Number(doctor?.active)
? `اولین نوبت آزاد: ${doctor.free_turn}` ? `اولین نوبت آزاد: ${moment(doctor?.free_turn * 1000).format(
"dddd jD jMMMM [ساعت] HH:mm"
)}`
: "نوبت ندارد"} : "نوبت ندارد"}
</p> </p>
</CustomLoading> </CustomLoading>
{/* Arrow */} {/* Arrow */}
<Link href={`/doctor/${doctor.id}`}> <Link href={`/doctor/${doctor?.uuid}`}>
<Button <Button
variant="contained" variant="contained"
className={`!p-[14px] !absolute !left-4 !bottom-4 !rounded-full !w-fit onClick={handleLoading}
${!doctor.free_turn || (loading && "!bg-[#D7D7D7]")} loading={doctor?.loading}
`} className="!p-[14px] !absolute !left-4 !bottom-4 !rounded-full !w-fit"
disabled={!doctor.free_turn || loading}
> >
<ArrowLeftD /> <div className={doctor && doctor.loading ? "opacity-0" : ""}>
<ArrowLeftD />
</div>
</Button> </Button>
</Link> </Link>
</li> </li>
+18
View File
@@ -0,0 +1,18 @@
import { CircularProgress } from "@mui/material";
import React from "react";
function LoadingComponent({ children, loading }) {
return (
<div className="relative">
{loading ? (
<div className="absolute min-h-[80px] mt-[160px] w-full h-full flex items-center justify-center">
<CircularProgress />
</div>
) : (
children
)}
</div>
);
}
export default LoadingComponent;
+2 -2
View File
@@ -2,7 +2,7 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
function Logo({ isAbsolute }) { function Logo({ isAbsolute, matchedCity }) {
return ( return (
<Link href="/" className={isAbsolute ? "absolute right-[32px] top-0" : ""}> <Link href="/" className={isAbsolute ? "absolute right-[32px] top-0" : ""}>
<div className="flex items-center justify-start gap-[5px]"> <div className="flex items-center justify-start gap-[5px]">
@@ -14,7 +14,7 @@ function Logo({ isAbsolute }) {
alt="logo" alt="logo"
/> />
<p className="text-[#526CAC] text-[14px] lg:text-[16px] font-bold font-kalame"> <p className="text-[#526CAC] text-[14px] lg:text-[16px] font-bold font-kalame">
نوبت ۷۲۴ {matchedCity?.site_name || "نوبت ۷۲۴"}
</p> </p>
</div> </div>
</Link> </Link>
@@ -2,12 +2,16 @@ import { styleDefault } from "@/mui";
import { Box, Button, Modal } from "@mui/material"; import { Box, Button, Modal } from "@mui/material";
import CloseModalD from "@/components/icons/CloseModalD"; import CloseModalD from "@/components/icons/CloseModalD";
import { removeToken } from "@/utils"; import { removeToken } from "@/utils";
import { useRouter } from "next/navigation";
import { useState } from "react";
function ModalLogout({ open, handleClose }) { function ModalLogout({ open, handleClose }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const logout = () => { const logout = () => {
setLoading(true);
removeToken(); removeToken();
window.location.pathname = '/login' router.replace("/login");
handleClose();
}; };
return ( return (
@@ -37,9 +41,10 @@ function ModalLogout({ open, handleClose }) {
انصراف انصراف
</Button> </Button>
<Button <Button
variant="contained"
onClick={logout} onClick={logout}
className="!py-[4px] md:!py-[6px] !min-h-[32px] md:!min-h-[39px] lg:!min-h-[46px] lg:!py-[8px] !px-[10px] md:!px-[14px] lg:!px-[18px] !rounded-[4px] !text-[#EFEFEF] !text-[12px] md:!text-[14px] lg:!text-[16px] !font-medium" loading={loading}
variant="contained"
className="!py-[4px] md:!py-[6px] !min-h-[32px] md:!min-h-[39px] lg:!min-h-[46px] lg:!py-[8px] !px-[10px] md:!px-[14px] lg:!px-[18px] !rounded-[4px] !text-[12px] md:!text-[14px] lg:!text-[16px] !font-medium"
> >
خروج خروج
</Button> </Button>
+2 -2
View File
@@ -3,7 +3,7 @@ function Pageguide({ list }) {
<ul className="flex items-center justify-start gap-1"> <ul className="flex items-center justify-start gap-1">
{list.map((item, idx) => ( {list.map((item, idx) => (
<li key={idx} className="flex items-center justify-start gap-1"> <li key={idx} className="flex items-center justify-start gap-1">
<p <h2
className={`text-[16px] font-normal className={`text-[16px] font-normal
${ ${
list.length > idx + 1 list.length > idx + 1
@@ -13,7 +13,7 @@ function Pageguide({ list }) {
`} `}
> >
{item} {item}
</p> </h2>
<span <span
className={` className={`
text-[#9B9B9B] text-[16px] font-normal text-[#9B9B9B] text-[16px] font-normal
+9 -3
View File
@@ -1,11 +1,17 @@
import { Pagination as PaginationMUI } from "@mui/material"; import { Pagination as PaginationMUI } from "@mui/material";
function Pagination() { function Pagination({ page, setPage, list, itemsPerPage }) {
const handleChange = (_, value) => setPage(value);
const count =
list && itemsPerPage ? Math.ceil(list.length / itemsPerPage) : 20;
return ( return (
<PaginationMUI <PaginationMUI
// count={doctors.length / 12} org count={count}
count={20} page={page || 1}
onChange={setPage && handleChange}
color="primary" color="primary"
className={count < 2 && "!hidden"}
sx={{ sx={{
"& .mui-8q7g72-MuiButtonBase-root-MuiPaginationItem-root": { "& .mui-8q7g72-MuiButtonBase-root-MuiPaginationItem-root": {
color: "#616161", color: "#616161",
+2 -2
View File
@@ -8,7 +8,7 @@ function ProgressDetail({ data }) {
{data.label} {data.label}
</p> </p>
<LinearProgress <LinearProgress
value={data.progress} value={data}
variant="determinate" variant="determinate"
className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]" className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]"
sx={{ sx={{
@@ -19,7 +19,7 @@ function ProgressDetail({ data }) {
}} }}
/> />
<p className="text-[#525252] text-[20px] font-medium"> <p className="text-[#525252] text-[20px] font-medium">
{numberToArStyle(data.progress)}% {numberToArStyle(data)}%
</p> </p>
</li> </li>
); );
+7 -4
View File
@@ -1,7 +1,7 @@
import { Slider } from "@mui/material"; import { Slider } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
function ProgressChange({ data }) { function ProgressChange({ data, changeData, parent }) {
const [value, setValue] = useState(70); const [value, setValue] = useState(70);
return ( return (
@@ -10,9 +10,12 @@ function ProgressChange({ data }) {
{data.label} {data.label}
</p> </p>
<Slider <Slider
defaultValue={value} defaultValue={data.progress}
className="!w-full lg:!w-[265px] !rounded-[10px] !h-[11px]" className="!w-full lg:!w-[265px] !rounded-[10px] !h-[11px]"
onChange={(e) => setValue(e.target.value)} onChange={(e) => {
setValue(e.target.value);
changeData(parent, data.name, e.target.value);
}}
sx={{ sx={{
"& .MuiSlider-rail": { "& .MuiSlider-rail": {
background: "#E8E8E8", background: "#E8E8E8",
@@ -32,7 +35,7 @@ function ProgressChange({ data }) {
}, },
}} }}
/> />
<p className="text-[#525252] text-[20px] font-medium w-[30px] min-w-[32px]"> <p className="text-[#525252] text-[20px] font-medium w-[30px] min-w-[32px] text-center">
{value} {value}
</p> </p>
</li> </li>
+5 -4
View File
@@ -7,7 +7,7 @@ function a11yProps(index) {
}; };
} }
function Tabs({ value, handleChange, listTab, isSm, style }) { function Tabs({ value, handleChange, listTab, inactives, isSm, style }) {
return ( return (
<Box className="!border-none !w-full"> <Box className="!border-none !w-full">
<TabsMUI <TabsMUI
@@ -24,14 +24,15 @@ function Tabs({ value, handleChange, listTab, isSm, style }) {
<Tab <Tab
key={idx} key={idx}
label={item} label={item}
disabled={inactives && inactives.includes(idx)}
{...a11yProps(idx)} {...a11yProps(idx)}
sx={{ sx={{
"&.MuiButtonBase-root": { "&.MuiButtonBase-root": {
width: !isSm && "fit-content !important", width: !isSm ? "fit-content !important" : undefined, // Fix: Replace `false` with `undefined`
minWidth: !isSm && "fit-content !important", minWidth: !isSm ? "fit-content !important" : undefined,
}, },
"&.MuiButtonBase-root:first-child": { "&.MuiButtonBase-root:first-child": {
marginLeft: !isSm && "9px !important", marginLeft: !isSm ? "9px !important" : undefined,
}, },
}} }}
className={` className={`
+1 -1
View File
@@ -47,7 +47,7 @@ function TimePickerField({ dataChange, isVacation, disable, timeStart, data }) {
const hours = e.$H; const hours = e.$H;
const minute = e.$m; const minute = e.$m;
const newValue = `${handleTwoLength(hours)}:${handleTwoLength( const newValue = `${handleTwoLength(hours)}:${handleTwoLength(
minute minute,
)}`; )}`;
const newData = data; const newData = data;
+1 -1
View File
@@ -18,7 +18,7 @@ function TimePickerInput({ changeData, handleDisableHours, name, data }) {
const hours = e.$H; const hours = e.$H;
const minute = e.$m; const minute = e.$m;
const newValue = `${handleTwoLength(hours)}:${handleTwoLength( const newValue = `${handleTwoLength(hours)}:${handleTwoLength(
minute minute,
)}`; )}`;
// setValue(newValue); // setValue(newValue);
changeData(newValue, name); changeData(newValue, name);
+27
View File
@@ -0,0 +1,27 @@
import ArrowUpComment from "@/components/icons/ArrowUpComment";
import { Button } from "@mui/material";
function AnswerField({ data, update, setUpdate }) {
return (
<div className="flex items-center justify-start gap-1 my-2">
<Button
className="!text-[#0FA1B3] !text-[12px] !font-medium"
onClick={() => {
data.is_open_reply = !data.is_open_reply;
setUpdate(!update);
}}
>
مشاهده پاسخ ها ({data.replies.length})
<div
className={`!mr-1 ${
data.is_open_reply ? "!rotate-0" : "!rotate-180"
}`}
>
<ArrowUpComment />
</div>
</Button>
</div>
);
}
export default AnswerField;
+2 -1
View File
@@ -1,12 +1,13 @@
import { TextField } from "@mui/material"; import { TextField } from "@mui/material";
function Field({ title, type, multiline, isPlaceHolder }) { function Field({ title, type, updateValue, value, multiline, isPlaceHolder }) {
return ( return (
<TextField <TextField
placeholder={isPlaceHolder && title} placeholder={isPlaceHolder && title}
label={!isPlaceHolder ? title : ""} label={!isPlaceHolder ? title : ""}
type={type || "text"} type={type || "text"}
className="!w-full !mx-auto !bg-transparent" className="!w-full !mx-auto !bg-transparent"
onChange={(e) => updateValue(e.target.value)}
sx={{ sx={{
"& .MuiFormLabel-root": { "& .MuiFormLabel-root": {
top: "-4px !important", top: "-4px !important",
+123 -139
View File
@@ -1,184 +1,168 @@
import Image from "next/image"; import Image from "next/image";
import { Button, Skeleton } from "@mui/material"; import { Button, CircularProgress } from "@mui/material";
import Field from "./Field";
// Icons // Icons
import BoldDisLikeComment from "@/components/icons/BoldDisLikeComment"; import BoldDisLikeComment from "@/components/icons/BoldDisLikeComment";
import BoldLikeComment from "@/components/icons/BoldLikeComment"; import BoldLikeComment from "@/components/icons/BoldLikeComment";
import DisLikeComment from "@/components/icons/DisLikeComment"; import DisLikeComment from "@/components/icons/DisLikeComment";
import ArrowUpComment from "@/components/icons/ArrowUpComment";
import LikeComment from "@/components/icons/LikeComment"; import LikeComment from "@/components/icons/LikeComment";
import Replies from "./Replies";
import AnswerField from "./AnswerField";
import SendAnswer from "./SendAnswer";
import { alert, isUserLoggedIn, timeAgo } from "@/helper";
import { request } from "@/services/response";
import { useState } from "react";
function ItemUser({ function ItemUser({ update, setUpdate, data }) {
update, const [loadingLike, setLoadingLike] = useState(false);
setUpdate, const [loadingDislike, setLoadingDislike] = useState(false);
isReply,
list, const SendReq = async (type) => {
setList, if (type === "like") setLoadingLike(true);
data, else setLoadingDislike(true);
loading,
}) { const body = {
const handleLike = (lengthLike, data, nameLike, nameIsLike) => { comment_id: data.id,
if (nameIsLike === "is_like" && data.is_dislike) { like: type === "like" ? 1 : 0,
data.is_dislike = false; };
data.dislike--;
} else if (nameIsLike === "is_dislike" && data.is_like) { try {
data.is_like = false; const res = await request.postCommentsLike(body);
data.like--; } catch (err) {
// err
} finally {
if (type === "like") setLoadingLike(false);
else setLoadingDislike(false);
} }
data[nameLike] = lengthLike; };
data[nameIsLike] = !data[nameIsLike];
const handleLike = (data, type) => {
SendReq(type);
const { current_user_like } = data.like_status;
if (type === "like") {
if (current_user_like.like) {
data.like_status.like_count -= 1;
current_user_like.like = false;
} else {
data.like_status.like_count += 1;
current_user_like.like = true;
if (current_user_like.dislike) {
data.like_status.dislike_count -= 1;
current_user_like.dislike = false;
}
}
} else {
if (current_user_like.dislike) {
data.like_status.dislike_count -= 1;
current_user_like.dislike = false;
} else {
data.like_status.dislike_count += 1;
current_user_like.dislike = true;
if (current_user_like.like) {
data.like_status.like_count -= 1;
current_user_like.like = false;
}
}
}
setUpdate(!update); setUpdate(!update);
}; };
return ( return (
<li className="w-full"> <li className="w-full">
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
{loading ? ( <Image
<Skeleton className="!w-[32px] !h-[32px]"
variant="circular" src={data.author?.picture[0]?.url}
className="!min-w-[40px] !min-h-[40px]" width={40}
/> height={40}
) : ( alt="profile"
<Image />
className="!w-[32px] !h-[32px] sm:!w-[35px] sm:!h-[35px] md:!h-[37px] md:!w-[37px] lg:!w-[40px] lg:!h-[40px]"
src={data.profile}
width={40}
height={40}
alt="profile"
/>
)}
<div className="flex flex-col items-start justify-center gap-[8px]"> <div className="flex flex-col items-start justify-center gap-[8px]">
{loading ? ( <p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
<Skeleton variant="text" className="!min-w-[70px]" /> {data.author.real_name}
) : ( </p>
<> <p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium"> {timeAgo(data.created)}
{data.name} </p>
</p>
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
3 روز پیش
</p>
</>
)}
</div> </div>
</div> </div>
<div className="mt-2"> <div className="mt-2">
{loading ? ( <p className="text-[#495057] text-[12px] md:text-[14px]">
<> {data.comment}
<Skeleton variant="text" className="!w-full !mt-8" /> </p>
<Skeleton variant="text" className="!w-1/3" />
</>
) : (
<p className="text-[#495057] text-[12px] md:text-[14px] font-normal leading-[20px] sm:leading-[24px] md:leading-[28px] lg:leading-[30px]">
{data.comment}
</p>
)}
<div className="flex items-center justify-start mt-2 gap-[28px]"> <div className="flex items-center justify-start mt-2 gap-[28px]">
{/* لایک */}
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={() => onClick={() => handleLike(data, "like")}
handleLike(
data.is_like ? data.like - 1 : data.like + 1,
data,
"like",
"is_like"
)
}
> >
{data.is_like ? <BoldLikeComment /> : <LikeComment />} {loadingLike ? (
<CircularProgress
className="!w-[24px] !h-[24px]"
sx={{ color: "#7E7E7E" }}
/>
) : data.like_status.current_user_like.like ? (
<BoldLikeComment />
) : (
<LikeComment />
)}
</div> </div>
{loading ? ( <p className="text-[#6C757D] text-[12px] select-none">
<Skeleton variant="text" className="!min-w-[8px]" /> {data.like_status.like_count || ""}
) : ( </p>
<p className="text-[#6C757D] text-[12px] font-normal select-none">
{data.like}
</p>
)}
</div> </div>
{/* دیس‌لایک */}
<div className="flex items-center justify-start gap-1"> <div className="flex items-center justify-start gap-1">
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={() => onClick={() => handleLike(data, "dislike")}
handleLike(
data.is_dislike ? data.dislike - 1 : data.dislike + 1,
data,
"dislike",
"is_dislike"
)
}
> >
{data.is_dislike ? <BoldDisLikeComment /> : <DisLikeComment />} {loadingDislike ? (
<CircularProgress
className="!w-[24px] !h-[24px]"
sx={{ color: "#7E7E7E" }}
/>
) : data.like_status.current_user_like.dislike ? (
<BoldDisLikeComment />
) : (
<DisLikeComment />
)}
</div> </div>
{loading ? ( <p className="text-[#6C757D] text-[12px] select-none">
<Skeleton variant="text" className="!min-w-[8px]" /> {data.like_status.dislike_count || ""}
) : ( </p>
<p className="text-[#6C757D] text-[12px] font-normal select-none">
{data.dislike}
</p>
)}
</div> </div>
<Button <Button
className="!text-[#495057] !text-[12px] !font-medium" className="!text-[#495057] !text-[12px] !font-medium"
onClick={() => { onClick={() => {
data.is_open_answer = !data.is_open_answer; if (isUserLoggedIn()) {
setUpdate(!update); data.is_open_answer = !data.is_open_answer;
setUpdate(!update);
} else {
alert(
"warning",
"برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید."
);
}
}} }}
variant="text" variant="text"
> >
پاسخ پاسخ
</Button> </Button>
</div> </div>
<div
className={`flex flex-col mt-2 justify-center overflow-hidden transition duration-500 items-start gap-2 mb-2 <SendAnswer data={data} />
${data.is_open_answer ? "max-h-fit" : "max-h-0"}`} {!!data.replies.length && (
> <AnswerField data={data} update={update} setUpdate={setUpdate} />
<Field type="text" title="پاسخ شما..." isPlaceHolder={true} />
<Button
variant="contained"
className="!p-2 !h-10 !text-[12px] !font-medium !text-[#FFF] sm:!py-[10px] md:!px-[12px] !rounded-[8px] !bg-[#5559CE]
!shadow-none sm:!shadow-[0px_4px_30px_0px_rgba(56,_160,_162,_0.30)]"
>
ارسال پاسخ
</Button>
</div>
{!!data.replays.length && (
<div className="flex items-center justify-start gap-1 my-2">
<Button
className="!text-[#0FA1B3] !text-[12px] !font-medium"
onClick={() => {
data.is_open_reply = !data.is_open_reply;
setUpdate(!update);
}}
>
مشاهده پاسخ ها ({data.replays.length})
<div
className={`!mr-1 ${
data.is_open_reply ? "!rotate-0" : "!rotate-180"
}`}
>
<ArrowUpComment />
</div>
</Button>
</div>
)} )}
<ul <Replies data={data} update={update} setUpdate={setUpdate} />
className={`flex mr-12 flex-col justify-center transition-all duration-500 overflow-hidden items-start gap-12
${loading && "w-full"}
${data.is_open_reply ? "max-h-screen mt-12" : "max-h-0"}`}
>
{data.replays.map((item, idx) => (
<ItemUser
key={idx}
data={item}
isReply={true}
update={update}
loading={loading}
setUpdate={setUpdate}
/>
))}
</ul>
</div> </div>
</li> </li>
); );
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import ItemUser from "./ItemUser";
function Replies({ data, update, setUpdate }) {
return (
<ul
className={`flex pr-12 flex-col justify-center transition-all duration-500 overflow-hidden items-start gap-12 w-full
${data.is_open_reply ? "max-h-fit mt-12" : "max-h-0"}`}
>
{data.replies.map((item, idx) => (
<ItemUser
key={idx}
data={item}
isReply={true}
update={update}
setUpdate={setUpdate}
/>
))}
</ul>
);
}
export default Replies;
+65
View File
@@ -0,0 +1,65 @@
import { Button } from "@mui/material";
import Field from "./Field";
import { useState } from "react";
import { request } from "@/services/response";
import { alert } from "@/helper";
function SendAnswer({ data }) {
const [comment, setComment] = useState("");
const [loading, setLoading] = useState(false);
const resetData = () => {
setComment("");
setLoading(false);
data.is_open_answer = false;
};
const sendReq = () => {
setLoading(true);
const body = {
comment,
doctor_id: data?.doctor?.id,
parent: data?.parent,
};
request
.postDoctorComment(body)
.then(() => {
alert(
"success",
"کامنت شما با موفقیت ثبت شد و پس از تأیید نمایش داده می‌شود."
);
resetData();
})
.catch(() => {
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
resetData();
});
};
return (
<div
className={`flex flex-col mt-2 justify-center overflow-hidden transition duration-500 items-start gap-2 mb-2
${data.is_open_answer ? "max-h-fit" : "max-h-0"}`}
>
<Field
type="text"
value={comment}
title="پاسخ شما..."
isPlaceHolder={true}
updateValue={(val) => setComment(val)}
/>
<Button
loading={loading}
onClick={sendReq}
variant="contained"
className="!p-2 !h-10 !text-[12px] !font-medium sm:!py-[10px] md:!px-[12px] !rounded-[8px]
!shadow-none sm:!shadow-[0px_4px_30px_0px_rgba(56,_160,_162,_0.30)]"
>
ارسال پاسخ
</Button>
</div>
);
}
export default SendAnswer;
+5 -8
View File
@@ -2,8 +2,8 @@
import { useState } from "react"; import { useState } from "react";
import ItemUser from "./ItemUser"; import ItemUser from "./ItemUser";
function Comment({ data, loading }) { function Comment({ data, comments }) {
const [list, setList] = useState(data.comments); const [list, setList] = useState(data?.comments);
const [update, setUpdate] = useState(false); const [update, setUpdate] = useState(false);
return ( return (
@@ -12,16 +12,13 @@ function Comment({ data, loading }) {
false && "w-full" false && "w-full"
}`} }`}
> >
{data && {comments &&
!!data.comments.length && comments.length &&
data.comments.map((item, idx) => ( comments.map((item, idx) => (
<ItemUser <ItemUser
key={idx} key={idx}
list={list}
data={item} data={item}
update={update} update={update}
setList={setList}
loading={loading}
setUpdate={setUpdate} setUpdate={setUpdate}
/> />
))} ))}
@@ -0,0 +1,16 @@
import { Skeleton } from "@mui/material";
import React from "react";
function LoadingDate({ loading }) {
if (loading) {
return (
<Skeleton
variant="rectangular"
className="!w-[300px] !rounded-[6px] !absolute !top-0 !right-1/2 !translate-x-1/2 !z-10"
height={352}
/>
);
}
}
export default LoadingDate;
@@ -1,21 +0,0 @@
import { Button } from "@mui/material";
import Image from "next/image";
function NextIconDatePicker({ nextIconDatePicker }) {
return (
<Button
className="!min-w-0 !p-0 !rounded-full"
ref={nextIconDatePicker}
onClick={(e) => e.preventDefault()}
>
<Image
src="/assets/icons/arrow-right.svg"
className="icon-left-datepicker"
height={24}
width={24}
/>
</Button>
);
}
export default NextIconDatePicker;
@@ -1,21 +0,0 @@
import { Button } from "@mui/material";
import Image from "next/image";
function PrevIconDatePicker({ prevIconDatePicker }) {
return (
<Button
className="!min-w-0 !p-0 !rounded-full !rotate-180"
ref={prevIconDatePicker}
onClick={(e) => e.preventDefault()}
>
<Image
src="/assets/icons/arrow-right.svg"
className="icon-left-datepicker"
height={24}
width={24}
/>
</Button>
);
}
export default PrevIconDatePicker;
@@ -0,0 +1,42 @@
import React from "react";
import { DatePicker } from "jalaali-react-date-picker";
import { nextIconData, prevIconData } from "./dataIcon";
import LoadingDate from "../LoadingDate";
import Image from "next/image";
function FirstDatePicker({
loading,
startDate,
disableDates,
handleDisableDate,
nextIconDatePickerF,
prevIconDatePickerF,
prevIconDatePickerS,
handleStartDateChange,
}) {
return (
<div className="w-full first md:w-1/2 lg:w-full xl:w-1/2 relative flex justify-center">
<DatePicker
value={startDate}
className={`w-full ${loading ? "opacity-0" : "opacity-100"}`}
timePicker={false}
nextIcon={
<Image {...nextIconData} ref={nextIconDatePickerF} id="nextIconF" />
}
prevIcon={
<Image
{...prevIconData}
ref={prevIconDatePickerF}
onClick={() => prevIconDatePickerS.current.click()}
/>
}
isFirst={true}
onChange={handleStartDateChange}
disabledDates={(e) => handleDisableDate(e)}
/>
<LoadingDate loading={loading} />
</div>
);
}
export default FirstDatePicker;
@@ -0,0 +1,42 @@
import React from "react";
import { DatePicker } from "jalaali-react-date-picker";
import { nextIconData, prevIconData } from "./dataIcon";
import LoadingDate from "../LoadingDate";
import Image from "next/image";
function SecondDatePicker({
endDate,
loading,
handleDisableDate,
nextIconDatePickerF,
nextIconDatePickerS,
prevIconDatePickerS,
handleEndDateChange,
}) {
return (
<div className="w-1/2 second hidden md:flex lg:hidden xl:flex">
<div className="w-full relative flex justify-center">
<DatePicker
value={endDate}
timePicker={false}
className={`w-full ${loading ? "opacity-0" : "opacity-100"}`}
nextIcon={
<Image
{...nextIconData}
ref={nextIconDatePickerS}
onClick={() => !loading && nextIconDatePickerF.current.click()}
/>
}
prevIcon={
<Image {...prevIconData} ref={prevIconDatePickerS} id="prevIconS" />
}
onChange={handleEndDateChange}
disabledDates={(e) => handleDisableDate(e)}
/>
<LoadingDate loading={loading} />
</div>
</div>
);
}
export default SecondDatePicker;
@@ -0,0 +1,16 @@
export const fixedIconData = {
src: "/assets/icons/arrow-right.svg",
alt: "arrow",
height: 24,
width: 24,
};
export const nextIconData = {
...fixedIconData,
className: "icon-left-datepicker cursor-pointer",
};
export const prevIconData = {
...fixedIconData,
className: "icon-left-datepicker cursor-pointer rotate-180",
};
+61 -66
View File
@@ -1,97 +1,92 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { DatePicker as DatePickerJalali } from "jalaali-react-date-picker"; import FirstDatePicker from "./date/FirstDatePicker";
import { Skeleton } from "@mui/material"; import SecondDatePicker from "./date/SecondDatePicker";
import NextIconDatePicker from "./NextIconDatePicker"; import { dateToTimestamp } from "@/helper";
import PrevIconDatePicker from "./PrevIconDatePicker"; import { request } from "@/services/response";
import moment from "moment-jalaali";
function DatePicker({ date, updateDate }) { function DatePicker({ setDate }) {
const nextIconDatePicker = useRef(); const nextIconDatePickerF = useRef();
const prevIconDatePicker = useRef(); const prevIconDatePickerF = useRef();
const nextIconDatePickerS = useRef();
const prevIconDatePickerS = useRef();
const [startDate, setStartDate] = useState(null); const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(); const [endDate, setEndDate] = useState();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [disableDates, setDisableDates] = useState([]);
const handleStartDateChange = (date) => { const handleStartDateChange = (date, e) => {
if (date) { if (date) {
setStartDate(date); setDate(dateToTimestamp(date));
setEndDate(null); setEndDate(null);
setStartDate(date);
} }
}; };
const handleEndDateChange = (date) => { const handleEndDateChange = (date) => {
if (date) { if (date) {
setDate(dateToTimestamp(date));
setEndDate(date); setEndDate(date);
setStartDate(null); setStartDate(null);
} }
}; };
const handleDisableDate = (e) => { const handleDisableDate = (e) => {
const today = new Date(); const date = moment(e).startOf("day");
return today >= e._d; const today = moment().startOf("day");
if (date.isBefore(today)) return true;
if (
disableDates.some((ts) => date.isSame(moment.unix(ts).startOf("day")))
) {
return true;
}
return false;
}; };
useEffect(() => { useEffect(() => {
if (nextIconDatePicker.current) { request
nextIconDatePicker.current.click(); .getAppointmentNotAvailable(47)
setLoading(false); .then((res) => {
setTimeout(() => {
setLoading(false);
}, 1000);
setDisableDates(res.data);
})
.catch((err) => {
});
if (nextIconDatePickerS.current) {
nextIconDatePickerS.current.click();
} }
}, [nextIconDatePicker]); }, [nextIconDatePickerS]);
return ( return (
<div className="flex items-start custom-datepicker justify-evenly w-full"> <div className="datePickerApt flex items-start custom-datepicker justify-evenly w-full">
<div className="w-full md:w-1/2 lg:w-full xl:w-1/2 relative flex justify-center"> <FirstDatePicker
<DatePickerJalali loading={loading}
value={startDate} startDate={startDate}
className={`w-full ${loading ? "opacity-0" : "opacity-100"}`} disableDates={disableDates}
timePicker={false} handleDisableDate={handleDisableDate}
nextIcon={ nextIconDatePickerF={nextIconDatePickerF}
<div className="flex md:hidden lg:flex xl:hidden"> prevIconDatePickerF={prevIconDatePickerF}
<NextIconDatePicker nextIconDatePicker={nextIconDatePicker} /> prevIconDatePickerS={prevIconDatePickerS}
</div> handleStartDateChange={handleStartDateChange}
} />
prevIcon={ <SecondDatePicker
<PrevIconDatePicker prevIconDatePicker={prevIconDatePicker} /> loading={loading}
} endDate={endDate}
onChange={handleStartDateChange} handleDisableDate={handleDisableDate}
disabledDates={(e) => handleDisableDate(e)} nextIconDatePickerF={nextIconDatePickerF}
/> nextIconDatePickerS={nextIconDatePickerS}
{loading && ( prevIconDatePickerS={prevIconDatePickerS}
<Skeleton handleEndDateChange={handleEndDateChange}
variant="rectangular" />
className="!w-[300px] !rounded-[6px] !absolute !top-0 !right-1/2 !translate-x-1/2 !z-10"
height={352}
/>
)}
</div>
<div className="w-1/2 hidden md:flex lg:hidden xl:flex">
<div className="w-full relative flex justify-center">
<DatePickerJalali
value={endDate}
timePicker={false}
className={`w-full ${loading ? "opacity-0" : "opacity-100"}`}
nextIcon={
<NextIconDatePicker nextIconDatePicker={nextIconDatePicker} />
}
prevIcon={
<div className="flex md:hidden lg:flex xl:hidden">
<PrevIconDatePicker prevIconDatePicker={prevIconDatePicker} />
</div>
}
onChange={handleEndDateChange}
disabledDates={(e) => handleDisableDate(e)}
/>
{loading && (
<Skeleton
variant="rectangular"
className="!w-[300px] !rounded-[6px] !absolute !top-0 !right-1/2 !translate-x-1/2 !z-10"
height={352}
/>
)}
</div>
</div>
</div> </div>
); );
} }
-32
View File
@@ -1,32 +0,0 @@
import { Button } from "@mui/material";
function Hours({ doctor, hour, setHour, value, nameHours }) {
return (
<ul className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-x-[16px] gap-y-[12px] sm:gap-y-[16px] md:gap-y-[20px] lg:gap-y-[24px] mt-[24px] mb-[40px]">
{doctor[nameHours[value]].map((item, idx) => (
<li key={idx}>
<Button
onClick={() => setHour(idx)}
disabled={!item.active}
className={`
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
${idx === hour ? "!bg-[#F79463] " : ""}
`}
>
<p
className={`
text-[16px] font-semibold text-[#525252]
${item.active ? "text-[#525252]" : "text-[#D7D7D7]"}
${idx === hour ? "!text-[#FAFAFA] " : ""}
`}
>
{item.hour}
</p>
</Button>
</li>
))}
</ul>
);
}
export default Hours;
+48
View File
@@ -0,0 +1,48 @@
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import { request } from "@/services/response";
import { Button } from "@mui/material";
import Cookies from "js-cookie";
import { useParams } from "next/navigation";
import { useState } from "react";
function SendAppo({ hour, setStep }) {
const params = useParams();
const doctorId = params.doctorId;
const [loading, setLoading] = useState(false);
const sendReq = () => {
const isLogged = Cookies.get("access_token");
if (isLogged) {
setStep((step) => step + 1);
}
// setLoading(true);
// request
// .postAppointment({
// doctor_id: doctorId,
// slot: hour,
// })
// .then(() => {
// setLoading(false);
// })
// .catch(() => {
// //
// });
};
return (
<div className="w-full flex justify-end">
<Button
className="!gap-[4px] !text-[16px] !font-medium !shadow-none !px-[12px] !py-[8px] md:!py-[9px] !w-full md:!w-fit !min-w-[157px]"
variant="contained"
loading={loading}
onClick={sendReq}
disabled={!hour}
>
تایید نوبت
<ArrowLeftB />
</Button>
</div>
);
}
export default SendAppo;
+45
View File
@@ -0,0 +1,45 @@
import { Button } from "@mui/material";
import React from "react";
function List({ appo, hour, setHour, value }) {
const list = appo && appo[value ? "evening" : "morning"];
const checkHour = (item) => JSON.stringify(item) === JSON.stringify(hour);
if (list && list.length) {
return (
<ul className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-x-[16px] gap-y-[12px] sm:gap-y-[16px] md:gap-y-[20px] lg:gap-y-[24px] mt-[24px] mb-[40px]">
{list.map((item, idx) => (
<li key={idx}>
<Button
onClick={() => setHour(item)}
disabled={!item.status === "available"}
className={`
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
${checkHour(item) ? "!bg-[#F79463] " : ""}
`}
>
<p
className={`
text-[16px] font-semibold text-[#525252]
${item.status === "available" ? "text-[#525252]" : "text-[#D7D7D7]"}
${checkHour(item) ? "!text-[#FAFAFA] " : ""}
`}
>
{item.time}
</p>
</Button>
</li>
))}
</ul>
);
} else {
return (
<p className="text-[#525252] text-[14px] md:text-[16px] font-medium py-[56px]">
در حال حاضر، نوبتی برای ساعتهای {[value ? "بعد از ظهر" : "صبح"]} موجود
نمیباشد.
</p>
);
}
}
export default List;
@@ -0,0 +1,23 @@
import { CircularProgress } from "@mui/material";
function Loading() {
return (
<div className="w-full grid place-items-center py-[48px]">
<CircularProgress
sx={{
"& .MuiCircularProgress-circle": {
stroke: "#F17732",
// strokeLinecap: "round",
},
"& .MuiCircularProgress-svg": {
// borderRadius: "50%",
// boxShadow: "inset 0 0 0 11px #D7D7D7",
},
}}
className="!mx-auto"
/>
</div>
);
}
export default Loading;
@@ -0,0 +1,18 @@
import List from "./List";
import Loading from "./Loading";
function Hours({ appo, doctor, hour, setHour, value, nameHours }) {
if (appo === "loading") {
return <Loading />;
} else if (typeof appo === "string") {
return (
<p className="text-[#525252] text-[14px] md:text-[16px] font-medium py-[56px]">
{appo}
</p>
);
} else {
return <List appo={appo} hour={hour} value={value} setHour={setHour} />;
}
}
export default Hours;
+34 -24
View File
@@ -1,19 +1,44 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import Hours from "./Hours"; import Hours from "./hours";
import Tabs from "../../Tabs"; import Tabs from "../../Tabs";
import { Button } from "@mui/material"; import { request } from "@/services/response";
import ArrowLeftB from "@/components/icons/ArrowLeftB"; import { useParams } from "next/navigation";
import SendAppo from "./SendAppo";
const listTab = ["صبح", "بعد از ظهر"]; const listTab = ["صبح", "بعد از ظهر"];
const nameHours = ["hours_morning", "hours_afternoon"]; const nameHours = ["hours_morning", "hours_afternoon"];
function DateTime({ doctor, setStep, setIsError, locateVisit }) { function DateTime({ date, doctor, setStep }) {
const params = useParams();
const doctorId = params.doctorId;
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [hour, setHour] = useState(0); const [hour, setHour] = useState();
const [appo, setAppo] = useState("تاریخ مورد نظرتان را انتخاب کنید.");
const handleChange = (event, newValue) => setValue(newValue); const handleChange = (event, newValue) => {
setHour();
setValue(newValue);
};
useEffect(() => {
if (date) {
setAppo("loading");
setHour();
request
.getAppointment(doctorId, date)
.then((res) => {
setAppo(res);
if (res.morning?.length && res.evening?.length) setValue(0);
else if (res.morning?.length && value !== 0) setValue(0);
else if (res.evening?.length && value !== 1) setValue(1);
})
.catch((err) => {
setAppo("در حال حاضر، نوبتی برای ساعت‌های صبح موجود نمی‌باشد.");
});
}
}, [date]);
return ( return (
<div className="w-full"> <div className="w-full">
@@ -41,29 +66,14 @@ function DateTime({ doctor, setStep, setIsError, locateVisit }) {
handleChange={handleChange} handleChange={handleChange}
/> />
<Hours <Hours
appo={appo}
hour={hour} hour={hour}
value={value} value={value}
doctor={doctor} doctor={doctor}
setHour={setHour} setHour={setHour}
nameHours={nameHours} nameHours={nameHours}
/> />
<div className="w-full flex justify-end"> <SendAppo hour={hour} setStep={setStep} />
<Button
className="!gap-[4px] !shadow-none !px-[12px] !py-[8px] md:!py-[9px] !w-full md:!w-fit !min-w-[157px]"
onClick={() => {
if (locateVisit) {
localStorage.setItem("doctor-id", doctor?.id);
setStep((prev) => prev + 1);
} else {
setIsError(true);
}
}}
variant="contained"
>
<p className="text-[#EFEFEF] text-[16px] font-medium">تایید نوبت</p>
<ArrowLeftB />
</Button>
</div>
</div> </div>
); );
} }
+33 -6
View File
@@ -2,14 +2,42 @@ import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material"; import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui"; import { styleTextSelectRight } from "@/mui";
function AutoCompleteSelect({ list, isError, updateData, name, label }) { function AutoCompleteSelect({
list,
value,
isError,
updateData,
label,
disabled = false,
listboxProps,
sendAll,
name,
}) {
return ( return (
<Autocomplete <Autocomplete
disablePortal disablePortal
options={list} options={list}
disabled={disabled}
ListboxProps={{
style: {
maxHeight: 200,
overflow: "auto",
},
}}
value={value || null}
getOptionLabel={(option) => {
if (typeof option === "string") return option;
return option?.name || option?.label || "";
}}
className="!w-full !rounded-lg auto-complete-selector" className="!w-full !rounded-lg auto-complete-selector"
onChange={(event) => { onChange={(e, newValue) => {
updateData && updateData(event.target.innerText, "value", name); updateData &&
updateData(
newValue && sendAll
? newValue
: (newValue && !sendAll && newValue.label) || "",
name
);
}} }}
sx={{ sx={{
...styleTextSelectRight, ...styleTextSelectRight,
@@ -51,11 +79,10 @@ function AutoCompleteSelect({ list, isError, updateData, name, label }) {
<Button <Button
fullWidth fullWidth
{...props} {...props}
style={{}} key={option.id || props.id}
key={props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]" className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
> >
{option.label} {option.name || option.label}
</Button> </Button>
)} )}
renderInput={(params) => ( renderInput={(params) => (
+98
View File
@@ -0,0 +1,98 @@
import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui";
function AutoSelector({
list,
value,
isError,
updateData,
label,
disabled = false,
listboxProps,
name,
}) {
return (
<Autocomplete
disablePortal
options={list}
disabled={disabled}
value={value || ""}
ListboxProps={listboxProps}
getOptionLabel={(option) => {
if (typeof option === "string") return option;
return option?.name || option?.label || "";
}}
className="!w-full !rounded-lg auto-complete-selector"
onChange={(e, newValue) => {
updateData(name, newValue?.label || newValue || "");
}}
sx={{
...styleTextSelectRight,
// Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
"& .MuiOutlinedInput-notchedOutline": {
border: isError ? "1px solid red !important" : "",
},
}}
renderOption={(props, option) => (
<Button
fullWidth
{...props}
key={option.id || props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
>
{option.name || option.label}
</Button>
)}
renderInput={(params) => (
<TextField
fullWidth
{...params}
InputProps={{
...params.InputProps,
endAdornment: (
<div className="absolute left-4">
<BottomSelect />
</div>
),
}}
placeholder={label}
sx={{
borderRadius: "8px",
}}
/>
)}
/>
);
}
export default AutoSelector;
+18 -3
View File
@@ -3,17 +3,30 @@ import { styleTextSelectRight } from "@/mui";
function Field({ function Field({
title, title,
error,
placeholder,
inputProps,
type, type,
dir, dir,
// isPlaceHolder value,
updateState,
}) { }) {
return ( return (
<TextField <TextField
label={title} label={title}
error={error}
dir={dir || "rtl"} dir={dir || "rtl"}
type={type || "text"} type={type || "text"}
value={value}
onChange={(e) => updateState(e.target.value)}
className="!w-full !mx-auto !bg-[#FFF]" className="!w-full !mx-auto !bg-[#FFF]"
InputProps={inputProps}
placeholder={placeholder || ""}
sx={{ sx={{
".MuiOutlinedInput-notchedOutline.muirtl-1d3z3hw-MuiOutlinedInput-notchedOutline":
{
border: error && "1px solid red !important",
},
...styleTextSelectRight, ...styleTextSelectRight,
// Hide Icon Number // Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button": "& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
@@ -23,8 +36,10 @@ function Field({
"& input[type=number]": { "& input[type=number]": {
MozAppearance: "textfield", MozAppearance: "textfield",
}, },
// "& .MuiFormLabel-root": { top: "-7px !important" }, "& .MuiInputBase-input": {
"& .MuiInputBase-input": { padding: "12.5px 14px" }, padding: "12.5px 14px",
color: "#0009",
},
"& .MuiInputBase-root": { borderRadius: "6px !important" }, "& .MuiInputBase-root": { borderRadius: "6px !important" },
"& .MuiOutlinedInput-notchedOutline": { "& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #E9ECEF !important", border: "1px solid #E9ECEF !important",
@@ -20,9 +20,9 @@ function EditField({
<TextField <TextField
{...props} {...props}
rows={multiline} rows={multiline}
value={data?.value}
multiline={multiline} multiline={multiline}
type={type || "text"} type={type || "text"}
value={data?.value || ""}
placeholder={placeholder || ""} placeholder={placeholder || ""}
disabled={noDisable ? false : !data?.isEdit} disabled={noDisable ? false : !data?.isEdit}
className={`!w-full res-field-account dark:!bg-transparent ${ className={`!w-full res-field-account dark:!bg-transparent ${
@@ -1,16 +1,29 @@
import { TextField } from "@mui/material"; import { TextField } from "@mui/material";
function Field({ title, type, props, multiline, isPlaceHolder }) { function Field({
type,
name,
title,
props,
value,
multiline,
handleChange,
isPlaceHolder,
}) {
return ( return (
<TextField <TextField
{...props} {...props}
size="small" size="small"
placeholder={isPlaceHolder && title} defaultValue={value || ""}
label={!isPlaceHolder ? title : ""}
type={type || "text"} type={type || "text"}
multiline={!!multiline} multiline={!!multiline}
rows={!!multiline ? 4 : 1} rows={!!multiline ? 4 : 1}
label={!isPlaceHolder ? title : ""}
placeholder={isPlaceHolder && title}
className="!w-full !mx-auto !bg-[#FFF]" className="!w-full !mx-auto !bg-[#FFF]"
onChange={(e) => {
handleChange(name || "", e.target.value);
}}
sx={{ sx={{
"& .MuiFormLabel-root": { "& .MuiFormLabel-root": {
top: "-4px !important", top: "-4px !important",
+6 -6
View File
@@ -3,14 +3,14 @@ function HeadPageList({ children, title, detail }) {
<div className="bg-head-menu !bg-no-repeat !bg-cover !bg-center"> <div className="bg-head-menu !bg-no-repeat !bg-cover !bg-center">
<div <div
className=" className="
padding-responsive flex flex-col items-center gap-[25px] padding-responsive flex flex-col items-center gap-[25px]
pt-[calc(12px_+_75px_+_32px)] sm:pt-[calc(21px_+_75px_+_45px)] md:pt-[calc(30px_+_75px_+_58px)] lg:pt-[calc(40px_+_75px_+_78px)] pt-[calc(12px_+_75px_+_32px)] sm:pt-[calc(21px_+_75px_+_45px)] md:pt-[calc(30px_+_75px_+_58px)] lg:pt-[calc(40px_+_75px_+_78px)]
pb-[62px] sm:pb-[69px] md:pb-[76px] lg:pb-[85px] pb-[62px] sm:pb-[69px] md:pb-[76px] lg:pb-[85px]
" "
> >
<p className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] text-center font-bold"> <h1 className="text-[#3B3B3B] text-[16px] sm:text-[19px] md:text-[22px] lg:text-[24px] text-center font-bold">
{title} {title}
</p> </h1>
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] text-center font-normal"> <p className="text-[#3B3B3B] text-[14px] md:text-[16px] text-center font-normal">
{detail} {detail}
</p> </p>
+2 -1
View File
@@ -4,12 +4,13 @@ function MultilineLoading({ loading, width, height, children, line }) {
return loading return loading
? Array(line) ? Array(line)
.fill({}) .fill({})
.map((_) => ( .map((_, idx) => (
<Skeleton <Skeleton
className={`${width === "full" ? "!w-full" : ""} !my-2`} className={`${width === "full" ? "!w-full" : ""} !my-2`}
variant="rounded" variant="rounded"
height={height} height={height}
width={width} width={width}
key={idx}
/> />
)) ))
: children; : children;
@@ -0,0 +1,93 @@
import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui";
function AddressSelector({
list,
value,
isError,
updateData,
label,
disabled,
listboxProps,
name,
}) {
return (
<Autocomplete
disablePortal
options={list}
disabled={disabled}
ListboxProps={listboxProps}
value={value || null}
getOptionLabel={(option) => option?.name || ""}
className="!w-full !rounded-lg auto-complete-selector"
onChange={(_, value) => updateData(name, value)}
sx={{
...styleTextSelectRight,
// Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
"& .MuiOutlinedInput-notchedOutline": {
border: isError ? "1px solid red !important" : "",
},
}}
renderOption={(props, option) => (
<Button
fullWidth
{...props}
key={props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
>
{option.name}
</Button>
)}
renderInput={(params) => (
<TextField
fullWidth
{...params}
InputProps={{
...params.InputProps,
endAdornment: (
<div className="absolute left-4">
<BottomSelect />
</div>
),
}}
placeholder={label}
sx={{
borderRadius: "8px",
}}
/>
)}
/>
);
}
export default AddressSelector;
@@ -0,0 +1,34 @@
// Icons
import ArrowBottomH from "@/components/icons/ArrowBottomH";
import Location from "@/components/icons/Location";
import { Button } from "@mui/material";
function ButtonFilter({ selected, setOpen }) {
const handleOpen = () => setOpen(true);
return (
<div>
<Button
className="!border-none hover:!bg-transparent !shadow-none !py-[11.5px] !min-w-fit !px-[7px] sm:!py-3 sm:!px-[9px] md:!px-[11px] lg:!px-[12px] !rounded-3"
onClick={handleOpen}
variant="contained"
color="secondary"
>
<div className="min-w-[20px] min-h-[20px] hidden md:flex">
<Location />
</div>
<p
className="text-[#616161] text-[11px] md:text-[14px] leading-[17px] sm:leading-[19px]
md:leading-[22px] lg:leading-[24px] font-medium ml-1 md:mx-1"
>
{selected.city || "شهر"}
</p>
<div className="w-[16px] h-[16px] md:w-[18px] md:h-[18px] lg:w-[20px] lg:h-[20px] grid place-items-center">
<ArrowBottomH />
</div>
</Button>
</div>
);
}
export default ButtonFilter;
+93
View File
@@ -0,0 +1,93 @@
import { Button } from "@mui/material";
// Icon
import CloseModalD from "@/components/icons/CloseModalD";
import ArrowLeftM from "@/components/icons/ArrowLeftM";
import { useState } from "react";
import AddressSelector from "./AddressSelector";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
function Content({
states,
filter,
sendReq,
setFilter,
handleClose,
setDataInURL,
filteredCities,
}) {
const [loading, setLoading] = useState(false);
const { matchedState } = getStateInfoClient();
const handleClick = () => {
setLoading(true);
sendReq().finally(() => {
setLoading(false);
handleClose();
});
};
const updateData = (name, value) => {
const newFilter =
name === "state"
? {
...filter,
state: value,
city: null,
}
: {
...filter,
[name]: value,
};
setDataInURL(newFilter);
setFilter(newFilter);
};
return (
<div>
<div className="flex justify-end w-full">
<div className="cursor-pointer" onClick={handleClose}>
<CloseModalD />
</div>
</div>
<div className="md:p-[24px] pt-0">
<p className="text-[#525252] mt-[38px] md:mt-4 text-[16px] font-medium text-center">
شهر یا استان مورد نظر را انتخاب نمایید:
</p>
<div className="flex md:min-w-[344px] flex-col mt-[52px] md:mt-[44px] mb-14 justify-center items-center gap-10">
<AddressSelector
name="state"
list={states}
label="استان"
disabled={matchedState}
value={filter?.state}
updateData={updateData}
/>
<AddressSelector
name="city"
label="شهر"
value={filter?.city}
list={filteredCities}
updateData={updateData}
disabled={!filter?.state}
/>
</div>
<div className="w-full flex justify-end">
<Button
loading={loading}
className="!w-full md:!w-fit !flex !px-3 items-center justify-center gap-1 mr-auto"
onClick={handleClick}
variant="contained"
color="primary"
disabled={!filter?.city || !filter?.state}
>
تایید و ادامه
<ArrowLeftM />
</Button>
</div>
</div>
</div>
);
}
export default Content;
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect } from "react";
import { Modal, Box } from "@mui/material";
import { styleDefault } from "@/mui";
// Data
import states from "@/data/state.json";
import cities from "@/data/city.json";
import Content from "./Content";
function ModalSearchCity({
open,
filter,
sendReq,
setOpen,
children,
setFilter,
setDataInURL,
}) {
const stateSelected = filter?.state?.name;
const [filteredCities, setFilteredCities] = useState([]);
const handleClose = () => setOpen(false);
useEffect(() => {
if (stateSelected) {
const selectedState = states.find(
(state) => state.name === stateSelected
);
if (selectedState) {
const stateId = selectedState.id;
const filteredCities = cities.filter(
(city) => city.province_id === stateId
);
setFilteredCities(filteredCities);
}
} else {
setFilteredCities([]);
}
}, [stateSelected]);
return (
<>
{/* Button Modal */}
{children}
{/* Content Modal */}
<Modal open={open} onClose={handleClose}>
<Box sx={styleDefault} className="!w-full !h-full md:!w-fit md:!h-fit">
<Content
states={states}
filter={filter}
sendReq={sendReq}
setFilter={setFilter}
handleClose={handleClose}
setDataInURL={setDataInURL}
filteredCities={filteredCities}
/>
</Box>
</Modal>
</>
);
}
export default ModalSearchCity;
+51 -20
View File
@@ -1,39 +1,70 @@
import CloseModalD from "@/components/icons/CloseModalD"; import CloseModalD from "@/components/icons/CloseModalD";
import Image from "next/image"; import Image from "next/image";
const maps = [ function Content({ data, onClose }) {
{ src: "/assets/images/snapp.png", name: "snapp" }, const { latitude, longitude } = data && data.map;
{ src: "/assets/images/tapsi.png", name: "tapsi" },
{ src: "/assets/images/maps.png", name: "maps" }, const maps = [
{ src: "/assets/images/balad.png", name: "balad" }, {
{ src: "/assets/images/waze.png", name: "waze" }, src: "/assets/images/snapp.png",
]; name: "snapp",
url: `https://snapp.ir/route?lat=${latitude}&lng=${longitude}`,
},
{
src: "/assets/images/tapsi.png",
name: "tapsi",
url: `https://tapsi.ir/route?lat=${latitude}&lng=${longitude}`,
},
{
src: "/assets/images/maps.png",
name: "maps",
url: `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`,
},
{
src: "/assets/images/balad.png",
name: "balad",
url: `https://balad.ir/map?lat=${latitude}&lng=${longitude}`,
},
{
src: "/assets/images/waze.png",
name: "waze",
url: `https://waze.com/ul?ll=${latitude},${longitude}&navigate=yes`,
},
];
function Content({ handleClose }) {
return ( return (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-medium"> <p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-medium">
باز شدن با باز شدن با
</p> </p>
<div className="cursor-pointer" onClick={handleClose}> <div className="cursor-pointer" onClick={onClose}>
<CloseModalD /> <CloseModalD />
</div> </div>
</div> </div>
<span className="bg-[#EFEFEF] block h-px w-full mt-[12px] mb-[22px]"></span> <span className="bg-[#EFEFEF] block h-px w-full mt-[12px] mb-[22px]"></span>
<ul className="flex items-center justify-center gap-[32px] md:gap-[36px] lg:gap-[40px] mx-auto flex-wrap md:flex-nowrap md:mx-[72px]"> <ul className="flex items-center justify-center gap-[32px] md:gap-[36px] lg:gap-[40px] mx-auto flex-wrap md:flex-nowrap md:mx-[72px]">
{maps.map((item, idx) => ( {maps.map((item, idx) => (
<li className="flex items-center flex-col justify-center gap-1"> <li
<Image key={idx}
className="w-[32px] h-[32px] lg:w-[40px] lg:h-[40px]" className="flex items-center flex-col justify-center gap-1"
src={item.src} >
alt="icon app" <a href={item.url} target="_blank" rel="noopener noreferrer">
height={40} <Image
width={40} className="
/> w-[32px] h-[32px] lg:w-[40px] lg:h-[40px]
<p className="text-[#3B3B3B] font-normal text-[14px]"> min-w-[32px] min-h-[32px] lg:min-w-[40px] lg:min-h-[40px]
{item.name} max-w-[32px] max-h-[32px] lg:max-w-[40px] lg:max-h-[40px]
</p> "
src={item.src}
alt="icon app"
height={40}
width={40}
/>
<p className="text-[#3B3B3B] font-normal text-[14px]">
{item.name}
</p>
</a>
</li> </li>
))} ))}
</ul> </ul>
+3 -3
View File
@@ -4,7 +4,7 @@ import { Drawer, Modal } from "@mui/material";
import { styleDefault } from "@/mui"; import { styleDefault } from "@/mui";
import Content from "./Content"; import Content from "./Content";
function ModalOpenLocation({ open, setOpen, handleClose, children }) { function ModalOpenLocation({ open, data, setOpen, handleClose, children }) {
const toggleDrawer = (newOpen) => () => { const toggleDrawer = (newOpen) => () => {
setOpen(newOpen); setOpen(newOpen);
}; };
@@ -19,7 +19,7 @@ function ModalOpenLocation({ open, setOpen, handleClose, children }) {
style={styleDefault} style={styleDefault}
className="!pt-[12px] !pb-[28px] !px-[24px] !bg-[#FAFAFA] !rounded-[8px] !overflow-hidden" className="!pt-[12px] !pb-[28px] !px-[24px] !bg-[#FAFAFA] !rounded-[8px] !overflow-hidden"
> >
<Content onClose={handleClose} /> <Content data={data} onClose={handleClose} />
</div> </div>
</Modal> </Modal>
<Drawer <Drawer
@@ -35,7 +35,7 @@ function ModalOpenLocation({ open, setOpen, handleClose, children }) {
}} }}
> >
<div className="!pt-[12px] !pb-[28px] !px-[24px] !bg-[#FAFAFA] !rounded-[8px] !overflow-hidden !shadow-[0px_1px_24.8px_0px_rgba(155,_155,_155,_0.27)]"> <div className="!pt-[12px] !pb-[28px] !px-[24px] !bg-[#FAFAFA] !rounded-[8px] !overflow-hidden !shadow-[0px_1px_24.8px_0px_rgba(155,_155,_155,_0.27)]">
<Content handleClose={handleClose} /> <Content data={data} handleClose={handleClose} />
</div> </div>
</Drawer> </Drawer>
</> </>
+86
View File
@@ -0,0 +1,86 @@
import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui";
function DefaultSelect({ list, value, updateData, label, error, name }) {
return (
<Autocomplete
disablePortal
options={list}
value={value || ""}
getOptionLabel={(option) => {
if (typeof option === "string") return option;
return option?.name || option?.label || "";
}}
className="!w-full !rounded-lg auto-complete-selector"
onChange={(_, newValue) => {
updateData(name, newValue?.label || newValue || "");
}}
sx={{
...styleTextSelectRight,
// Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: error
? "1px solid red !important"
: "1px solid #D7D7D7 !important",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: error ? "red !important" : "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
renderOption={(props, option) => (
<Button
fullWidth
{...props}
key={option.id || props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
>
{option.name || option.label}
</Button>
)}
renderInput={(params) => (
<TextField
fullWidth
{...params}
InputProps={{
...params.InputProps,
endAdornment: (
<div className="absolute left-4">
<BottomSelect />
</div>
),
}}
placeholder={label}
sx={{
borderRadius: "8px",
}}
/>
)}
/>
);
}
export default DefaultSelect;
+101
View File
@@ -0,0 +1,101 @@
import MenuItem from "@mui/material/MenuItem";
import ListSubheader from "@mui/material/ListSubheader";
import Select from "@mui/material/Select";
import { styleTextSelectRight } from "@/mui";
import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
function groupSpecialtiesByParent(specialties) {
const groups = [];
const parents = specialties.filter((item) => item.parent === null);
parents.forEach((parent) => {
const children = specialties.filter((item) => item.parent === parent.id);
if (children.length > 0) {
groups.push({
parent,
children,
});
}
});
return groups;
}
export default function GroupedSelect({
list,
name,
updateData,
data,
nameKey = "name",
}) {
const groupedList = groupSpecialtiesByParent(list);
const handleChange = (event) => {
const value = event.target.value;
updateData && updateData(value, "value", name);
};
return (
<div className="w-full auto-complete-selector">
<Select
value={data}
onChange={handleChange}
IconComponent={IconMultipleSelect}
MenuProps={{
style: {
maxHeight: 300,
},
}}
className="!w-full"
sx={{
...styleTextSelectRight,
"& .MuiSelect-icon": {
left: "16px",
right: "auto",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
>
{groupedList.map((group) => [
<ListSubheader
key={group.parent.id}
sx={{
backgroundColor: "#35343D",
color: "#fff",
fontSize: "14px",
}}
>
{group.parent[nameKey]}
</ListSubheader>,
...group.children.map((child) => (
<MenuItem key={child.id} value={child.id}>
{child[nameKey]}
</MenuItem>
)),
])}
</Select>
</div>
);
}
+72
View File
@@ -0,0 +1,72 @@
import OutlinedInput from "@mui/material/OutlinedInput";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import { styleTextSelectRight } from "@/mui";
import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
export default function MultipleSelect({ value, updateData, name, list }) {
const handleChange = (event) => {
const {
target: { value },
} = event;
const newVal = typeof value === "string" ? value.split(",") : value;
updateData(newVal, "value", name);
};
return (
<div className="auto-complete-selector w-full">
<Select
multiple
displayEmpty
value={value}
onChange={handleChange}
input={<OutlinedInput />}
IconComponent={IconMultipleSelect}
className=""
sx={{
...styleTextSelectRight,
width: "100% !important",
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
>
{list.map((item) => (
<MenuItem
key={item.id || item.uuid}
value={item.id}
selected={value.includes(item.id)}
className={`!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1]
${
value.includes(item.id)
? "dark:!bg-[#35343D] !bg-[#F1F1F1]"
: "dark:!bg-[#1F1D2B] !bg-[#FFF]"
}
hover:dark:!bg-[#35343D] hover:!bg-[#F5F5F5]`}
>
{item.name || item.label}
</MenuItem>
))}
</Select>
</div>
);
}
+12 -5
View File
@@ -1,15 +1,20 @@
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import Cropper from "react-easy-crop"; import Cropper from "react-easy-crop";
import { cropImage } from "./cropImg"; import { cropImage, dataURLtoFile } from "./cropImg";
function Crop({ data, uploadImg, handleClose }) { function Crop({ data, uploadImg, handleClose }) {
const [crop, setCrop] = useState({ x: 0, y: 0 }); const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1); const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const onComplete = (imagePromisse) => const onComplete = async (imagePromise, name) => {
imagePromisse.then((image) => uploadImg(image)); const dataUrl = await imagePromise;
const file = dataURLtoFile(dataUrl, name);
uploadImg({ blob: dataUrl, file });
};
return ( return (
<div className="mt-[16px]"> <div className="mt-[16px]">
@@ -18,7 +23,7 @@ function Crop({ data, uploadImg, handleClose }) {
<Cropper <Cropper
crop={crop} crop={crop}
zoom={zoom} zoom={zoom}
aspect={4 / 3} aspect={1}
image={data.blob} image={data.blob}
onCropChange={setCrop} onCropChange={setCrop}
onCropComplete={(_, croppedAreaPixels) => onCropComplete={(_, croppedAreaPixels) =>
@@ -38,7 +43,9 @@ function Crop({ data, uploadImg, handleClose }) {
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
onClick={() => onComplete(cropImage(data.blob, croppedAreaPixels))} onClick={() =>
onComplete(cropImage(data.blob, croppedAreaPixels), data.file?.name)
}
className="!text-[16px] !max-w-[calc(50%_-_8px)] !font-medium !shadow-none !py-[5px] md:!py-[7px] lg:!py-[9px]" className="!text-[16px] !max-w-[calc(50%_-_8px)] !font-medium !shadow-none !py-[5px] md:!py-[7px] lg:!py-[9px]"
> >
ذخیره ذخیره
+14
View File
@@ -37,6 +37,20 @@ async function getCroppedImg(imageSrc, pixelCrop) {
return canvas.toDataURL("image/jpeg"); return canvas.toDataURL("image/jpeg");
} }
export const dataURLtoFile = (dataurl, filename) => {
const arr = dataurl.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
export const cropImage = async (image, croppedAreaPixels, onError) => { export const cropImage = async (image, croppedAreaPixels, onError) => {
try { try {
const croppedImage = await getCroppedImg(image, croppedAreaPixels); const croppedImage = await getCroppedImg(image, croppedAreaPixels);
+1 -1
View File
@@ -54,7 +54,7 @@ function UploadFile({ image, setImage }) {
<Image <Image
width={24} width={24}
height={24} height={24}
src={image} src={image.blob}
alt="img-uploaded" alt="img-uploaded"
className="w-[74px] h-[74px] rounded-full overflow-hidden object-cover" className="w-[74px] h-[74px] rounded-full overflow-hidden object-cover"
/> />
+2 -2
View File
@@ -1,9 +1,9 @@
import ContactUsPage from "@/components/contactUs"; import ContactUsPage from "@/components/contactUs";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
function ContactUs() { function ContactUs() {
return ( return (
<Layout title="تماس با ما" name="contact-us"> <Layout name="/contact-us">
<ContactUsPage /> <ContactUsPage />
</Layout> </Layout>
); );
+22 -30
View File
@@ -1,37 +1,29 @@
"use client"; import Content from "@/components/dashboard/Content";
import { defineAbilitiesFor } from "@/lib/ability";
import { getUser } from "@/lib/auth";
import { getStateInfo } from "@/lib/getStateInfo";
import { removeToken } from "@/utils";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { useState } from "react"; export default async function Dashboard({ searchParams }) {
const { matchedCity } = getStateInfo();
const user = await getUser();
import userData from "@/data/userData.json"; const ability = defineAbilitiesFor(user);
import DashboardPage from "@/components/dashboard"; const cookieStore = cookies();
import UserAccountPage from "@/components/dashboard/userAccount/UserAccount"; const token = cookieStore.get("access_token");
function UserAccount() { if (!ability.can("access", "Dashboard")) {
const [value, setValue] = useState(0); removeToken();
const [isOpenSide, setIsOpenSide] = useState(false); return redirect("/login");
const [isTurnsDetails, setIsTurnsDetails] = useState(false); }
return ( return (
<DashboardPage <Content
value={value} logged={token.value}
setValue={setValue} params={searchParams}
user={userData.data} matchedCity={matchedCity}
isOpenSide={isOpenSide} />
setIsOpenSide={setIsOpenSide}
isTurnsDetails={isTurnsDetails}
setIsTurnsDetails={setIsTurnsDetails}
>
<UserAccountPage
value={value}
setValue={setValue}
user={userData.data}
isOpenSide={isOpenSide}
setIsOpenSide={setIsOpenSide}
isTurnsDetails={isTurnsDetails}
setIsTurnsDetails={setIsTurnsDetails}
/>
</DashboardPage>
); );
} }
export default UserAccount;
+30 -6
View File
@@ -1,13 +1,37 @@
import DoctorPage from "@/components/doctor"; import DoctorPage from "@/components/doctor";
import doctors from "@/data/doctors.json"; import Layout from "@/components/layout/StLayout";
import Layout from "@/components/layout"; import axios from "axios";
function Doctor({ params: { slug } }) { async function Doctor({ params: { slug } }) {
const doctor = doctors.find((item) => item.id === +slug); let doctors = null;
let doctor = null;
let comments = null;
const API_URL = process.env.NEXT_PUBLIC_API_URL;
try {
const resDoctors = await axios.get(
`${API_URL}/api/v1/doctors?active=1`
);
const resDoctor = await axios.get(
`${API_URL}/api/v1/doctor/${slug}`
);
doctor = resDoctor.data;
doctors = resDoctors.data.data;
if (doctor) {
const resComments = await axios.get(
`${API_URL}/api/v1/clinicpro/comments/${doctor.id}`
);
comments = resComments?.data?.data;
}
} catch (error) {
// console.error("problem with req:", error.message);
// return redirect("/unauthorized");
}
return ( return (
<Layout title="دکتر ها"> <Layout>
<DoctorPage doctor={doctor} /> <DoctorPage doctor={doctor} doctors={doctors} comments={comments} />
</Layout> </Layout>
); );
} }
+42 -4
View File
@@ -1,10 +1,48 @@
import DoctorsPage from "@/components/doctors"; import DoctorsPage from "@/components/doctors";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import { buildDoctorParams } from "@/helper";
import { getStateInfo } from "@/lib/getStateInfo";
import axios from "axios";
async function Doctors({ searchParams }) {
const { matchedCity, matchedState } = getStateInfo();
const API_URL = process.env.NEXT_PUBLIC_API_URL;
let doctors = null;
const stateParams = searchParams.state;
const cityParams = searchParams.city;
try {
let newSearchParams = searchParams;
// Conditional State
if (stateParams) newSearchParams.state = stateParams;
else if (matchedState) newSearchParams.state = matchedState.name;
// Conditional City
if (cityParams) newSearchParams.city = cityParams;
else if (matchedCity && matchedCity.id !== "63")
newSearchParams.city = matchedCity.name;
const params = buildDoctorParams(newSearchParams);
const response = await axios.get(`${API_URL}/api/v1/doctors`, {
params,
});
doctors = response.data.data;
} catch (error) {
console.error("Error fetching doctors:", error);
}
function Doctors() {
return ( return (
<Layout title="دکتر ها"> <Layout>
<DoctorsPage /> <DoctorsPage
list={doctors}
params={searchParams}
matchedCity={matchedCity}
matchedState={matchedState}
/>
</Layout> </Layout>
); );
} }
+158 -62
View File
@@ -2,62 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Font Irans Sans X */
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Thin.ttf);
font-weight: 100;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-ExtraBlack.ttf);
font-weight: 200;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Light.ttf);
font-weight: 300;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Regular.ttf);
font-weight: 400;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Medium.ttf);
font-weight: 500;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-DemiBold.ttf);
font-weight: 600;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Bold.ttf);
font-weight: 700;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-ExtraBold.ttf);
font-weight: 800;
}
@font-face {
font-family: iran-sans;
src: url(../public/fonts/IranSansX/FarsiNumerals/IRANSansXFaNum-Black.ttf);
font-weight: 900;
}
/* Font Kalame */ /* Font Kalame */
@font-face { @font-face {
@@ -114,8 +58,64 @@
font-weight: 900; font-weight: 900;
} }
/* Font Vazir */
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Thin.ttf);
font-weight: 100;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-ExtraLight.ttf);
font-weight: 200;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Light.ttf);
font-weight: 300;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Regular.ttf);
font-weight: 400;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Medium.ttf);
font-weight: 500;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-SemiBold.ttf);
font-weight: 600;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Bold.ttf);
font-weight: 700;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-ExtraBold.ttf);
font-weight: 800;
}
@font-face {
font-family: vazir;
src: url(../public/fonts/vazir/Vazirmatn-RD-FD-Black.ttf);
font-weight: 900;
}
body { body {
font-family: iran-sans; font-family: vazir;
padding: 0; padding: 0;
margin: 0; margin: 0;
background: #fafafa; background: #fafafa;
@@ -237,7 +237,7 @@ html {
} }
.MuiFormControl-root .MuiInputBase-root .MuiInputBase-input { .MuiFormControl-root .MuiInputBase-root .MuiInputBase-input {
font-size: 16px !important ; font-size: 16px !important;
} }
@keyframes openx { @keyframes openx {
@@ -295,7 +295,7 @@ html {
} }
.MuiModal-root div:nth-child(3) { .MuiModal-root div:nth-child(3) {
border-radius: 0 !important; /* border-radius: 0 !important; */
} }
} }
@@ -440,7 +440,14 @@ html {
.icon-keyboard_double_arrow_right, .icon-keyboard_double_arrow_right,
.icon-keyboard_double_arrow_left { .icon-keyboard_double_arrow_left {
display: none !important; color: transparent !important;
}
.icon-keyboard_double_arrow_right::before,
.icon-keyboard_double_arrow_left::before {
background: url(../public/assets/icons/double-right.svg);
width: 24px;
height: 24px;
} }
.icon-chevron-right::before, .icon-chevron-right::before,
@@ -451,7 +458,8 @@ html {
height: 24px; height: 24px;
} }
.icon-chevron-left::before { .icon-chevron-left::before,
.icon-keyboard_double_arrow_left::before {
rotate: 180deg; rotate: 180deg;
} }
@@ -706,6 +714,10 @@ html {
color: #fafafa; color: #fafafa;
} }
.MuiPaper-root .MuiList-root .MuiListSubheader-root {
background: #5559ce;
}
.panel-jalaali { .panel-jalaali {
width: 464px !important; width: 464px !important;
} }
@@ -792,7 +804,7 @@ html {
.Toastify__toast { .Toastify__toast {
padding: 12px 40px 12px 12px !important; padding: 12px 40px 12px 12px !important;
font-family: iran-sans !important; font-family: vazir !important;
} }
.Toastify__toast .Toastify__toast-body { .Toastify__toast .Toastify__toast-body {
@@ -846,3 +858,87 @@ html {
} }
/* End Of Toastify */ /* End Of Toastify */
.text-item-head-white:hover .item-head {
color: #d7d8ed;
}
.bg-poster {
background-image: url(../public/assets/images/poster.png);
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: cover;
}
.bg-stroke-circle {
background-image: url(../public/assets/images/stroke-circle.png);
background-size: cover;
}
.MuiButton-loading .MuiButton-loadingIndicator {
color: #ffffff !important;
}
@keyframes spin {
to {
transform: rotate(90deg);
}
}
.spin {
animation: spin 1s linear infinite;
}
/* date picker appointment */
.datePickerApt
.first
.panel-header-wrapper
.center:first-child
.iconItem:first-child,
.datePickerApt
.first
.panel-header-wrapper
.center:last-child
.iconItem:last-child,
.datePickerApt
.second
.panel-header-wrapper
.center:first-child
.iconItem:first-child,
.datePickerApt
.second
.panel-header-wrapper
.center:last-child
.iconItem:last-child {
display: none;
}
.datePickerApt #nextIconF,
.datePickerApt #prevIconS {
display: flex;
}
@media (min-width: 768px) {
.datePickerApt #nextIconF,
.datePickerApt #prevIconS {
display: none;
}
}
@media (min-width: 1024px) {
.datePickerApt #nextIconF,
.datePickerApt #prevIconS {
display: flex;
}
}
@media (min-width: 1280px) {
.datePickerApt #nextIconF,
.datePickerApt #prevIconS {
display: none;
}
}
/* end of date picker appointment */
+27 -15
View File
@@ -4,31 +4,43 @@ import "jalaali-react-date-picker/lib/styles/index.css";
import { Providers } from "./Providers"; import { Providers } from "./Providers";
import { GoogleAnalytics } from "@next/third-parties/google"; import { GoogleAnalytics } from "@next/third-parties/google";
import { GoogleTagManager } from "@next/third-parties/google"; import { GoogleTagManager } from "@next/third-parties/google";
// Toastify
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import CustomToastify from "./CustomToastify"; import CustomToastify from "./CustomToastify";
import { ProvinceProvider } from "@/context/ProvinceProvider";
import { getStateInfo } from "@/lib/getStateInfo";
export const metadata = { export async function generateMetadata() {
title: "نوبت724", const { matchedCity } = getStateInfo();
description:
"نوبت 724 - سیستم آنلاین نوبت‌دهی برای پزشکان و کلینیک‌ها. با استفاده از نوبت 724، به راحتی نوبت پزشکی خود را به صورت آنلاین رزرو کنید و از خدمات سریع و کارآمد ما بهره‌مند شوید", return {
keywords: title: matchedCity ? matchedCity.title : "نوبت724",
"نوبت 724, نوبت دهی, رزرو نوبت, پزشک, پزشکی, کلینیک, بیمارستان, نوبت آنلاین, سیستم نوبت دهی, سیستم نوبت دهی آنلاین, سیستم نوبت دهی پزشکی, سیستم نوبت دهی کلینیک, سیستم نوبت دهی بیمارستان, سیستم نوبت دهی آنلاین پزشکی, سیستم نوبت دهی آنلاین کلینیک, سیستم نوبت دهی آنلاین بیمارستان", description: matchedCity
image: "https://www.nobat724.com/assets/images/logo.png", ? matchedCity.description
}; : "نوبت 724 - سیستم آنلاین نوبت‌دهی برای پزشکان و کلینیک‌ها. با استفاده از نوبت 724، به راحتی نوبت پزشکی خود را به صورت آنلاین رزرو کنید و از خدمات سریع و کارآمد ما بهره‌مند شوید",
keywords: matchedCity
? matchedCity.keywords
: "نوبت 724, نوبت دهی, رزرو نوبت, پزشک, پزشکی, کلینیک, بیمارستان, نوبت آنلاین, سیستم نوبت دهی, سیستم نوبت دهی آنلاین, سیستم نوبت دهی پزشکی, سیستم نوبت دهی کلینیک, سیستم نوبت دهی بیمارستان, سیستم نوبت دهی آنلاین پزشکی, سیستم نوبت دهی آنلاین کلینیک, سیستم نوبت دهی آنلاین بیمارستان",
openGraph: {
images: ["https://www.nobat724.com/assets/images/logo.png"],
},
};
}
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="fa" dir="rtl" suppressHydrationWarning> <html lang="fa" dir="rtl" suppressHydrationWarning>
<meta <head>
name="viewport" <meta
content="width=device-width,initial-scale=1,maximum-scale=1" name="viewport"
/> content="width=device-width,initial-scale=1,maximum-scale=1"
/>
</head>
<ThemeRegistry> <ThemeRegistry>
<GoogleTagManager gtmId="GTM-NXBSV7GS" /> <GoogleTagManager gtmId="GTM-NXBSV7GS" />
<body className="bg-[#FAFAFA]"> <body className="bg-[#FAFAFA]">
<Providers>{children}</Providers> <ProvinceProvider>
<Providers>{children}</Providers>
</ProvinceProvider>
<CustomToastify /> <CustomToastify />
</body> </body>
<GoogleAnalytics gaId="G-HG3RZ1PJS5" /> <GoogleAnalytics gaId="G-HG3RZ1PJS5" />
+2 -20
View File
@@ -1,25 +1,7 @@
"use client"; import ContentVerify from "@/components/register/ContentVerify";
import RegisterPage from "@/components/register";
import SetCodePage from "@/components/register/SetCodePage";
import LogInPage from "@/components/register/LogInPage";
import { useState } from "react";
function LoginVerify() { function LoginVerify() {
const [isSendMsg, setIsSendMsg] = useState(false); return <ContentVerify />;
return (
<RegisterPage>
{isSendMsg ? (
<SetCodePage
setIsSendMsg={setIsSendMsg}
link={`/account/${localStorage.getItem("doctor-id")}`}
/>
) : (
<LogInPage setIsSendMsg={setIsSendMsg} />
)}
</RegisterPage>
);
} }
export default LoginVerify; export default LoginVerify;
+12 -18
View File
@@ -1,23 +1,17 @@
"use client"; import ContentLogin from "@/components/register/ContentLogin";
import RegisterPage from "@/components/register"; import { defineAbilitiesFor } from "@/lib/ability";
import SetCodePage from "@/components/register/SetCodePage"; import { getUser } from "@/lib/auth";
import LogInPage from "@/components/register/LogInPage"; import { redirect } from "next/navigation";
import { useState } from "react";
function LogIn() { async function LogIn() {
const [isSendMsg, setIsSendMsg] = useState(false); const user = await getUser();
const ability = defineAbilitiesFor(user);
return ( if (!ability.can("access", "Login")) {
<div> return redirect("/");
<RegisterPage> }
{isSendMsg ? (
<SetCodePage setIsSendMsg={setIsSendMsg} setStep={false} link="/" /> return <ContentLogin />;
) : (
<LogInPage setStep={false} setIsSendMsg={setIsSendMsg} />
)}
</RegisterPage>
</div>
);
} }
export default LogIn; export default LogIn;
+1 -1
View File
@@ -1,4 +1,4 @@
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import NotFoundPage from "@/components/notFound"; import NotFoundPage from "@/components/notFound";
function NotFound() { function NotFound() {
+2 -2
View File
@@ -1,9 +1,9 @@
import HomePage from "@/components/home"; import HomePage from "@/components/home";
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
export default function Home() { export default function Home() {
return ( return (
<Layout title="خانه" name=""> <Layout name="/">
<HomePage /> <HomePage />
</Layout> </Layout>
); );
-2
View File
@@ -1,5 +1,3 @@
"use client";
import DashboardPage from "@/components/panel/dashboard"; import DashboardPage from "@/components/panel/dashboard";
import Information from "@/data/information.json"; import Information from "@/data/information.json";
+11 -56
View File
@@ -1,62 +1,17 @@
"use client"; import Content from "@/components/panel/Content";
import { defineAbilitiesFor } from "@/lib/ability";
import { getUser } from "@/lib/auth";
import { redirect } from "next/navigation";
import { useEffect, useState } from "react"; async function LayoutPanel({ children }) {
import Header from "@/components/layoutPanel/header"; const user = await getUser();
import Sidebar from "@/components/layoutPanel/sidebar"; const ability = defineAbilitiesFor(user);
import Information from "@/data/information.json";
import UserDetail from "@/components/layoutPanel/userDetail";
import { usePathname } from "next/navigation";
import BgGray from "@/components/layoutPanel/sidebar/BgGray";
function LayoutPanel({ children }) { if (!ability.can("access", "Panel")) {
const pathname = usePathname(); return redirect("/");
const [active, setActive] = useState(false); }
const [open, setOpen] = useState(true);
const [loading, setLoading] = useState(true);
useEffect(() => { return <Content children={children} />;
setTimeout(() => {
setLoading(false);
}, 2000);
}, []);
return (
<div className="flex min-h-screen dark:bg-[#1F1D2B]">
<Sidebar
open={open}
active={active}
setOpen={setOpen}
setActive={setActive}
/>
<BgGray isActive={active} setIsActive={setActive} />
<main
className={`
w-full transition-all duration-500 dark:bg-[#1F1D2B]
${
pathname.includes("dashboard")
? open
? "md:min-w-[calc(100%_-_243px)] md:w-[calc(100%_-_243px)] xl:min-w-[calc(100%_-_243px_-_332px)] xl:w-[calc(100%_-_243px_-_332px)]"
: "md:min-w-[calc(100%_-_96px)] md:w-[calc(100%_-_96px)] xl:min-w-[calc(100%_-_96px_-_332px)] xl:w-[calc(100%_-_96px_-_332px)]"
: open
? "md:w-[calc(100%-243px)]"
: "md:w-[calc(100%-96px)]"
}
`}
>
<Header
data={Information}
setActive={setActive}
disableProfile={pathname.includes("dashboard")}
/>
<section className="my-[24px] opacity-element mx-[16px] bg-[#FAFAFA] dark:bg-[#1F1D2B]">
{children}
</section>
</main>
{pathname.includes("dashboard") && (
<UserDetail data={Information} loading={loading} />
)}
</div>
);
} }
export default LayoutPanel; export default LayoutPanel;
+1 -1
View File
@@ -3,7 +3,7 @@ import Information from "@/data/information.json";
import Bank from "@/data/bank.json"; import Bank from "@/data/bank.json";
function UserAccount() { function UserAccount() {
return <UserAccountPage data={{information: Information, bank: Bank}} />; return <UserAccountPage data={{ information: Information, bank: Bank }} />;
} }
export default UserAccount; export default UserAccount;
+2 -2
View File
@@ -1,9 +1,9 @@
import Layout from "@/components/layout"; import Layout from "@/components/layout/StLayout";
import SpecialtiesPage from "@/components/specialties"; import SpecialtiesPage from "@/components/specialties";
function Specialties() { function Specialties() {
return ( return (
<Layout title="تخصص ها" name="specialties"> <Layout name="/specialties">
<SpecialtiesPage /> <SpecialtiesPage />
</Layout> </Layout>
); );
+1 -1
View File
@@ -4,7 +4,7 @@ import { createTheme } from "@mui/material/styles";
const theme = createTheme({ const theme = createTheme({
direction: "rtl", direction: "rtl",
typography: { typography: {
fontFamily: "iran-sans", fontFamily: "vazir",
}, },
components: { components: {
MuiButton: { MuiButton: {
+6 -3
View File
@@ -1,7 +1,10 @@
import { getStateInfo } from "@/lib/getStateInfo";
import Image from "next/image"; import Image from "next/image";
import React from "react"; import React from "react";
function Head() { function Head() {
const { matchedCity } = getStateInfo();
return ( return (
<div className="bg-[#5559C2] rounded-none md:rounded-lg w-full py-[24px] flex flex-col items-center justify-center gap-[13px] md:gap-[16px]"> <div className="bg-[#5559C2] rounded-none md:rounded-lg w-full py-[24px] flex flex-col items-center justify-center gap-[13px] md:gap-[16px]">
<Image <Image
@@ -10,9 +13,9 @@ function Head() {
width={46} width={46}
alt="logo" alt="logo"
/> />
<p className="text-[#FAFAFA] text-[16px] sm:text-[18px] md:text-[22px] lg:text-[24px] font-bold"> <h1 className="text-[#FAFAFA] text-[16px] sm:text-[18px] md:text-[22px] lg:text-[24px] font-bold">
درباره نوبت ۷۲۴ درباره {matchedCity?.site_name}
</p> </h1>
</div> </div>
); );
} }
+1 -1
View File
@@ -15,7 +15,7 @@ function Services() {
return ( return (
<div className="mt-[16px] sm:mt-[24px] md:mt-[32px] lg:mt-[40px] px-[16px] sm:px-[12%]"> <div className="mt-[16px] sm:mt-[24px] md:mt-[32px] lg:mt-[40px] px-[16px] sm:px-[12%]">
<div className="flex justify-center"> <div className="flex justify-center">
<AnimationTextHead name="zoom-in-up" text="خدمات ما"> <AnimationTextHead text="خدمات ما">
<UnderlineLG /> <UnderlineLG />
</AnimationTextHead> </AnimationTextHead>
</div> </div>
+2 -2
View File
@@ -10,7 +10,7 @@ function AboutUsPage() {
" "
> >
<Head /> <Head />
<p <h2
className=" className="
text-[#525252] text-[16px] font-normal mt-[32px] leading-[24px] md:leading-[32px] lg:leading-[40px] text-[#525252] text-[16px] font-normal mt-[32px] leading-[24px] md:leading-[32px] lg:leading-[40px]
px-[16px] sm:px-[52px] md:px-[15%] text-justify md:text-center px-[16px] sm:px-[52px] md:px-[15%] text-justify md:text-center
@@ -26,7 +26,7 @@ function AboutUsPage() {
داشت که تمام و دشواری موجود در ارائه راهکارها، و شرایط سخت تایپ به پایان داشت که تمام و دشواری موجود در ارائه راهکارها، و شرایط سخت تایپ به پایان
رسد و زمان مورد نیاز شامل حروفچینی دستاوردهای اصلی، و جوابگوی سوالات رسد و زمان مورد نیاز شامل حروفچینی دستاوردهای اصلی، و جوابگوی سوالات
پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد. پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد.
</p> </h2>
<Services /> <Services />
</div> </div>
); );
+66
View File
@@ -0,0 +1,66 @@
import Date from "./date";
// Components
import Content from "./Content";
import Layout from "@/components/layout";
import Paying from "@/components/appointment/paying";
import Detail from "@/components/appointment/detail";
import FailedPay from "@/components/appointment/failedPay";
import SuccessPay from "@/components/appointment/successPay";
function Container({
step,
data,
doctor,
setData,
setStep,
prevData,
matchedCity,
isForAnother,
setIsForAnother,
}) {
const elements = [
<Date doctor={doctor} setStep={setStep} />,
// <LogInPage setIsSendMsg={false} setStep={setStep} />,
// <VerificationPage setIsSendMsg={false} link={false} setStep={setStep} />,
<Detail
data={data}
setStep={setStep}
setData={setData}
prevData={prevData}
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
/>,
<Paying setStep={setStep} />,
<SuccessPay setStep={setStep} />,
<FailedPay setStep={setStep} />,
];
return (
<>
{/* {step === 1 || step === 2 ? (
<RegisterPage>{elements[step]}</RegisterPage>
) : ( */}
<Layout
title="رزرو"
name="booking"
disableFooter={step > 2}
matchedCity={matchedCity}
paddingBottom="pb-[80px]"
>
<Content
disableSide={step === 5 || step === 6}
isFirst={step === 0}
setStep={setStep}
doctor={doctor}
step={step}
>
{elements[step]}
</Content>
</Layout>
{/* )} */}
</>
);
}
export default Container;
@@ -1,14 +1,7 @@
import Information from "./information"; import Information from "./information";
import Location from "./location"; import Location from "./location";
function BookingPage({ function Content({ doctor, step, setStep, children, isFirst, disableSide }) {
doctor,
step,
setStep,
children,
isFirst,
disableSide,
}) {
return ( return (
<div <div
className="flex flex-col lg:flex-row justify-center gap-[12px] sm:gap-[16px] md:gap-[20px] lg:gap-[24px] padding-responsive className="flex flex-col lg:flex-row justify-center gap-[12px] sm:gap-[16px] md:gap-[20px] lg:gap-[24px] padding-responsive
@@ -29,4 +22,4 @@ function BookingPage({
); );
} }
export default BookingPage; export default Content;
@@ -1,7 +1,7 @@
import DatePicker from "@/app/component/date/datePicker"; import DatePicker from "@/app/component/date/datePicker";
function SelectDatePicker() { function SelectDatePicker({ setDate }) {
return <DatePicker />; return <DatePicker setDate={setDate} />;
} }
export default SelectDatePicker; export default SelectDatePicker;
@@ -1,12 +1,12 @@
import SelectDatePicker from "./SelectDatePicker"; import SelectDatePicker from "./SelectDatePicker";
function Time({ isStep }) { function Time({ isStep, setDate }) {
return ( return (
<div className="flex relative mt-[24px] flex-col items-start gap-[19px] justify-start"> <div className="flex relative mt-[24px] flex-col items-start gap-[19px] justify-start">
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold"> <p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
2. انتخاب روز 1. انتخاب روز
</p> </p>
<SelectDatePicker /> <SelectDatePicker setDate={setDate} />
{!isStep && ( {!isStep && (
<div className="absolute left-0 top-0 w-full h-full bg-[rgba(255,255,255,0.84)]"></div> <div className="absolute left-0 top-0 w-full h-full bg-[rgba(255,255,255,0.84)]"></div>
)} )}
@@ -1,15 +1,15 @@
import DateTime from "@/app/component/date/dateTime"; import DateTime from "@/app/component/date/dateTime";
function Hour({ doctor, setStep, setIsError, locateVisit, isStep }) { function Hour({ doctor, setStep, date, locateVisit, isStep }) {
return ( return (
<div className="flex relative mt-[24px] flex-col items-start gap-[12px] justify-start"> <div className="flex relative mt-[24px] flex-col items-start gap-[12px] justify-start">
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold"> <p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
3. انتخاب ساعت 2. انتخاب ساعت
</p> </p>
<DateTime <DateTime
date={date}
doctor={doctor} doctor={doctor}
setStep={setStep} setStep={setStep}
setIsError={setIsError}
locateVisit={locateVisit} locateVisit={locateVisit}
/> />
{!isStep && ( {!isStep && (
@@ -1,11 +1,10 @@
import { useState } from "react"; import { useState } from "react";
import Hour from "./hour"; import Hour from "./hour";
import Time from "./Time"; import Time from "./Time";
import PlaceVisit from "./PlaceVisit";
function Date({ doctor, setStep }) { function Date({ doctor, setStep }) {
const [locateVisit, setLocateVisit] = useState(true); const [locateVisit, setLocateVisit] = useState(true);
const [isError, setIsError] = useState(false); const [date, setDate] = useState();
return ( return (
<div className="w-full lg:w-[61%]"> <div className="w-full lg:w-[61%]">
@@ -13,22 +12,18 @@ function Date({ doctor, setStep }) {
className="p-0 lg:p-[24px] rounded-[8px] border border-solid border-transparent lg:border-[#EFEFEF] className="p-0 lg:p-[24px] rounded-[8px] border border-solid border-transparent lg:border-[#EFEFEF]
bg-transparent lg:bg-[#FFF]" bg-transparent lg:bg-[#FFF]"
> >
<PlaceVisit
setLocateVisit={setLocateVisit}
locateVisit={locateVisit}
isError={isError}
/>
<Time <Time
isStep={doctor.multiwork ? locateVisit : true} setDate={setDate}
locateVisit={locateVisit} locateVisit={locateVisit}
isStep={doctor?.multiwork ? locateVisit : true}
/> />
<Hour <Hour
isStep={doctor.multiwork ? locateVisit : true} isStep={doctor?.multiwork ? locateVisit : true}
setLocateVisit={setLocateVisit} setLocateVisit={setLocateVisit}
locateVisit={locateVisit} locateVisit={locateVisit}
setIsError={setIsError}
setStep={setStep} setStep={setStep}
doctor={doctor} doctor={doctor}
date={date}
/> />
</div> </div>
</div> </div>
@@ -1,14 +1,16 @@
import AutoCompleteSelect from "@/app/component/element/AutoCompleteSelect";
import EditField from "../../../app/component/EditField";
import Content from "./Content"; import Content from "./Content";
import EditField from "../../../app/component/fields/EditField";
import DefaultSelect from "@/app/component/selectors/DefaultSelect";
const bime = [ function Form({ changeData, insurance, data, isForAnother }) {
{ label: "بیمه 1", id: 10 }, const findVal = () => {
{ label: "بیمه 2", id: 20 }, return insurance?.find(
{ label: "بیمه 3", id: 30 }, (g) =>
]; g.id ===
(data?.basic_insurance?.value ? data?.basic_insurance.value.id : false)
);
};
function Form({ changeData, data, updateSelect, isForAnother }) {
return ( return (
<div <div
className="grid mt-[12px] sm:mt-[14px] md:mt-[17px] lg:mt-[20px] items-start className="grid mt-[12px] sm:mt-[14px] md:mt-[17px] lg:mt-[20px] items-start
@@ -20,19 +22,20 @@ function Form({ changeData, data, updateSelect, isForAnother }) {
<Content title="کد ملی"> <Content title="کد ملی">
<EditField <EditField
changeData={changeData} changeData={changeData}
data={data?.codemeli} data={data?.national_code}
name="codemeli" name="national_code"
/> />
</Content> </Content>
<Content title="نام و نام خانوادگی"> <Content title="نام و نام خانوادگی">
<EditField changeData={changeData} data={data?.name} name="name" /> <EditField changeData={changeData} data={data?.name} name="name" />
</Content> </Content>
<Content title="نوع بیمه"> <Content title="نوع بیمه">
<AutoCompleteSelect <DefaultSelect
updateData={updateSelect} updateData={(name, val) => changeData(val, "value", name)}
label="نوع بیمه" label="نوع بیمه"
name="first" name="basic_insurance"
list={bime} list={insurance}
value={findVal()}
/> />
</Content> </Content>
</div> </div>
@@ -0,0 +1,63 @@
import { useState } from "react";
import ButtonFixed from "../paying/ButtonFixed";
import { Button } from "@mui/material";
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import { request } from "@/services/response";
function SubmitData({ setStep, data, prevData }) {
const [loading, setLoading] = useState(false);
const newStep = () => setStep((prev) => prev + 1);
const handleSubmit = () => {
const isChanged = JSON.stringify(prevData) !== JSON.stringify(data);
if (isChanged) {
setLoading(true);
let newData = data;
const { basic_insurance, name, national_code, uuid } = newData;
newData = {
basic_insurance: [basic_insurance.value?.id],
...(prevData.name?.value === data.name.value ? {} : { name: name }),
...(prevData.national_code?.value
? {}
: { national_code: national_code }),
};
request
.patchUserProfile(newData, uuid)
.then(() => {
setLoading(false);
newStep();
})
.catch(() => {
setLoading(false);
});
} else {
newStep();
}
};
return (
<ButtonFixed>
<Button
loading={loading}
className={`
!flex !w-full md:!w-fit !mt-0 md:!mt-[32px]
!gap-[4px] md:!mr-auto !px-[12px] !py-[8px] md:!py-[9px] !min-w-[150px]
`}
variant="contained"
onClick={handleSubmit}
>
<p
className={`${loading && "opacity-0"} text-[#EFEFEF] text-[16px] font-medium`}
>
ثبت اطلاعات
</p>
<ArrowLeftB />
</Button>
</ButtonFixed>
);
}
export default SubmitData;
@@ -1,9 +1,9 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import AddCircleBlueA from "@/components/icons/AddCircleBlueA"; import AddCircleBlueA from "@/components/icons/AddCircleBlueA";
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import ButtonFixed from "../paying/ButtonFixed";
import Form from "./Form"; import Form from "./Form";
import { request } from "@/services/response";
import SubmitData from "./SubmitData";
function Detail({ function Detail({
isForAnother, isForAnother,
@@ -11,14 +11,11 @@ function Detail({
setIsForAnother, setIsForAnother,
data, data,
setData, setData,
prevData,
fadeElement, fadeElement,
setIsPay, setIsPay,
}) { }) {
const [selected, setSelected] = useState({ const [insurance, setInsurance] = useState();
first: "",
second: "",
});
const changeData = (value, type, name) => const changeData = (value, type, name) =>
setData({ setData({
...data, ...data,
@@ -28,8 +25,13 @@ function Detail({
}, },
}); });
const updateSelect = (event, name) => useEffect(() => {
setSelected({ ...selected, [name]: event }); request.getInsuranceType().then((res) => {
if (res && res.data) {
setInsurance(res.data);
}
});
}, []);
return ( return (
// ${typePay === 'success' || typePay === 'failed' ? 'bg-transparent border-transparent md:bg-[#FFF] md:border-[#EFEFEF]' : ''} // ${typePay === 'success' || typePay === 'failed' ? 'bg-transparent border-transparent md:bg-[#FFF] md:border-[#EFEFEF]' : ''}
@@ -43,10 +45,10 @@ function Detail({
{isForAnother ? "اطلاعات کاربر جدید" : "اطلاعات حساب کاربری"} {isForAnother ? "اطلاعات کاربر جدید" : "اطلاعات حساب کاربری"}
</p> </p>
<Form <Form
isForAnother={isForAnother}
updateSelect={updateSelect}
changeData={changeData}
data={data} data={data}
insurance={insurance}
changeData={changeData}
isForAnother={isForAnother}
/> />
{!isForAnother && ( {!isForAnother && (
<Button <Button
@@ -66,25 +68,7 @@ function Detail({
</p> </p>
</Button> </Button>
)} )}
<ButtonFixed> <SubmitData setStep={setStep} data={data} prevData={prevData} />
<Button
className={`
!flex !w-full md:!w-fit
!gap-[4px] md:!mr-auto !px-[12px] !py-[8px] md:!py-[9px] !min-w-[150px]
${
isForAnother
? "!mt-0 md:!mt-[40px]"
: "!mt-0 md:!mt-[32px]"
}`}
variant="contained"
onClick={() => setStep((prev) => prev + 1)}
>
<p className="text-[#EFEFEF] text-[16px] font-medium">
ثبت اطلاعات
</p>
<ArrowLeftB />
</Button>
</ButtonFixed>
</div> </div>
</div> </div>
); );
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import doctors from "@/data/doctors.json";
import { request } from "@/services/response";
import Cookies from "js-cookie";
import Container from "./Container";
const defaultData = {
phone: { value: "", isEdit: false },
national_code: { value: "", isEdit: false },
name: { value: "", isEdit: false },
basic_insurance: { value: "", isEdit: false },
};
function AppointmentPage({ slug, matchedCity }) {
const doctor = doctors.find((item) => item.id === +slug);
const [step, setStep] = useState(0);
const [isForAnother, setIsForAnother] = useState(false);
const [prevData, setPrevData] = useState(defaultData);
const [data, setData] = useState(defaultData);
useEffect(() => {
const userInfo = Cookies.get("userInfo");
const parsedData = userInfo && JSON.parse(userInfo);
if (userInfo && parsedData) {
request
.getUserProfile(parsedData.uuid)
.then((res) => {
if (res) {
const newData = {
uuid: res.uuid,
phone: { value: parsedData.username || "", isEdit: false },
national_code: {
value: res.national_code || "",
isEdit: res.national_code ? false : true,
},
name: { value: `${res.name} ${res.family}`, isEdit: true },
basic_insurance: {
value: res.basic_insurance,
isEdit: true,
},
};
setData(newData);
setPrevData(newData);
} else {
const emptyData = {
uuid: res.uuid,
phone: { value: parsedData.username || "", isEdit: false },
national_code: { value: "", isEdit: true },
name: { value: "", isEdit: true },
basic_insurance: {
value: res.basic_insurance,
isEdit: true,
},
};
setData(emptyData);
setPrevData(emptyData);
}
})
.catch(() => {
//
});
}
}, []);
return (
<Container
step={step}
data={data}
doctor={doctor}
setData={setData}
setStep={setStep}
prevData={prevData}
matchedCity={matchedCity}
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
/>
);
}
export default AppointmentPage;
@@ -14,7 +14,7 @@ function Detail({ doctor, step, setStep }) {
<LocationA /> <LocationA />
</div> </div>
<p className="text-[#616161] text-[14px] md:text-[16px] font-normal"> <p className="text-[#616161] text-[14px] md:text-[16px] font-normal">
آدرس: {doctor.location} آدرس: {doctor?.location}
</p> </p>
</li> </li>
<li className="flex items-center justify-between w-full"> <li className="flex items-center justify-between w-full">
@@ -16,16 +16,16 @@ function Head({ doctor }) {
h-[40px] sm:h-[58px] md:h-[77px] lg:h-[95px] h-[40px] sm:h-[58px] md:h-[77px] lg:h-[95px]
" "
alt="profile doctor" alt="profile doctor"
src={doctor.img} src={doctor?.img}
height={95} height={95}
width={95} width={95}
/> />
<div className="flex flex-col items-start justify-center gap-[8px] sm:gap-[12px] md:gap-[16px] lg:gap-[20px]"> <div className="flex flex-col items-start justify-center gap-[8px] sm:gap-[12px] md:gap-[16px] lg:gap-[20px]">
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold"> <p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
{doctor.name} {doctor?.name}
</p> </p>
<p className="text-[#616161] text-[14px] md:text-[16px] font-medium"> <p className="text-[#616161] text-[14px] md:text-[16px] font-medium">
تخصص: {doctor.expertise} تخصص: {doctor?.expertise}
</p> </p>
</div> </div>
</div> </div>
@@ -19,7 +19,9 @@ function Information({ doctor, step, setStep, typePay, isPay, disableSide }) {
} }
`} `}
> >
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">جزئیات نوبت</p> <p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
جزئیات نوبت
</p>
<Head doctor={doctor} /> <Head doctor={doctor} />
<Detail doctor={doctor} step={step} setStep={setStep} /> <Detail doctor={doctor} step={step} setStep={setStep} />
</div> </div>
@@ -1,7 +1,7 @@
function Address({ doctor }) { function Address({ doctor }) {
return ( return (
<ul className="hidden mt-[4px] lg:flex flex-col"> <ul className="hidden mt-[4px] lg:flex flex-col">
{doctor.address.map((item, idx) => ( {doctor?.address?.map((item, idx) => (
<li key={idx}> <li key={idx}>
<div className="flex py-[12px] text-[#616161] text-[14px] items-start justify-start"> <div className="flex py-[12px] text-[#616161] text-[14px] items-start justify-start">
<p className="font-bold min-w-[115px]">{`${item.name}: `}</p> <p className="font-bold min-w-[115px]">{`${item.name}: `}</p>
@@ -8,18 +8,18 @@ function Head({ doctor }) {
w-[48px] sm:w-[51px] md:w-[54px] lg:w-[56px] w-[48px] sm:w-[51px] md:w-[54px] lg:w-[56px]
h-[48px] sm:h-[51px] md:h-[54px] lg:h-[56px] h-[48px] sm:h-[51px] md:h-[54px] lg:h-[56px]
" "
src={doctor.img} src={doctor?.img}
alt="profile" alt="profile"
height={56} height={56}
width={56} width={56}
/> />
<div className="flex flex-col items-start justify-start gap-[8px]"> <div className="flex flex-col items-start justify-start gap-[8px]">
<p className="text-[#3B3B3B] text-[12px] md:text-[14px] font-bold"> <h2 className="text-[#3B3B3B] text-[12px] md:text-[14px] font-bold">
{doctor.name} {doctor?.name}
</p> </h2>
<p className="text-[#525252] text-[12px] md:text-[14px] font-medium"> <h3 className="text-[#525252] text-[12px] md:text-[14px] font-medium">
تخصص: {doctor.expertise} تخصص: {doctor?.expertise}
</p> </h3>
</div> </div>
</div> </div>
); );

Some files were not shown because too many files have changed in this diff Show More