mearge dev
This commit is contained in:
@@ -34,3 +34,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.idx/dev.nix
|
||||
.vscode/settings.json
|
||||
.env
|
||||
|
||||
+48
@@ -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"]
|
||||
@@ -29,8 +29,6 @@ npm install
|
||||
|
||||
## 🛠 Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
@@ -55,6 +53,16 @@ npm run lint
|
||||
|
||||
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
|
||||
|
||||
- ✅ Responsive design using MUI and Tailwind
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import AboutUsPage from "@/components/aboutUs";
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
|
||||
function AboutUs() {
|
||||
return (
|
||||
<Layout title="درباره ما" name="about-us">
|
||||
<Layout name="/about-us">
|
||||
<AboutUsPage />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -1,13 +1,16 @@
|
||||
import BlogPage from "@/components/blog";
|
||||
import Layout from "@/components/layout";
|
||||
import articles from "@/data/articles.json";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import { fetchReq } from "@/lib/req";
|
||||
|
||||
function Blog({ params: { slug } }) {
|
||||
const article = articles.find((item) => item.id === +slug);
|
||||
async function Blog({ params: { 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 (
|
||||
<Layout title="وبلاگ" name="blog">
|
||||
<BlogPage article={article} articles={articles} />
|
||||
<Layout>
|
||||
<BlogPage blog={blog} blogs={blogs.blogs} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-6
@@ -1,12 +1,16 @@
|
||||
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 (
|
||||
<Layout title="وبلاگ ها" name="blogs">
|
||||
<BlogsPage />
|
||||
<Layout name="/blogs">
|
||||
<BlogsPage blogs={blogs} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blogs;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,16 +1,26 @@
|
||||
import ClinicPage from "@/components/clinic";
|
||||
import Layout from "@/components/layout";
|
||||
import clinics from "@/data/clinics.json";
|
||||
import doctors from "@/data/doctors.json";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import { fetchReq } from "@/lib/req";
|
||||
|
||||
function Blog({ params: { slug } }) {
|
||||
const clinic = clinics.find((item) => item.id === +slug);
|
||||
async function Clinic({ params: { 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 (
|
||||
<Layout title="کلینیک" name="clinic">
|
||||
<Layout>
|
||||
<ClinicPage data={clinic} doctors={doctors} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Blog;
|
||||
export default Clinic;
|
||||
|
||||
+16
-6
@@ -1,12 +1,22 @@
|
||||
// app/clinics/page.js
|
||||
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 (
|
||||
<Layout title="کلینیک ها" name="clinics">
|
||||
<ClinicsPage />
|
||||
<Layout name="/clinics">
|
||||
<ClinicsPage
|
||||
clinics={clinics && clinics.data}
|
||||
matchedCity={matchedCity}
|
||||
matchedState={matchedState}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Clinics;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
function AnimationTextHead({ name, text, children }) {
|
||||
function AnimationTextHead({ text, children }) {
|
||||
return (
|
||||
<div className="flex justify-center items-start relative w-fit">
|
||||
<div className="overflow-hidden mb-[4px] md:mb-[5px] lg:mb-[6px]">
|
||||
<p
|
||||
data-aos={name}
|
||||
className="text-[#3B3B3B] text-nowrap overflow-hidden text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold"
|
||||
>
|
||||
<p className="text-[#3B3B3B] text-nowrap overflow-hidden text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { Button, Skeleton } from "@mui/material";
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
// Icons
|
||||
import ArrowLeftD from "@/components/icons/ArrowLeftD";
|
||||
@@ -10,44 +10,65 @@ import Link from "next/link";
|
||||
import CustomLoading from "./loading/Custom";
|
||||
import TextLoading from "./loading/Text";
|
||||
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 (
|
||||
<li className="p-4 relative rounded-lg bg-[#FFF] border border-[#EFEFEF]">
|
||||
{/* Detail Doctor */}
|
||||
<div className="flex items-center justify-start gap-2.5">
|
||||
<CircularLoading loading={loading} width={66} height={66}>
|
||||
<CircularLoading width={66} height={66}>
|
||||
<Image
|
||||
width={72}
|
||||
height={72}
|
||||
src={doctor.img}
|
||||
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>
|
||||
<div className="flex flex-col items-start justify-center gap-2">
|
||||
<TextLoading width={90} height={15} loading={loading}>
|
||||
<p className="text-[#3B3B3B] text-[14px] font-bold">
|
||||
{doctor.name}
|
||||
</p>
|
||||
<TextLoading width={90} height={15}>
|
||||
<Link href={`/doctor/${doctor?.uuid}`}>
|
||||
<p className="text-[#3B3B3B] text-[14px] font-bold">
|
||||
{doctor?.name}
|
||||
</p>
|
||||
</Link>
|
||||
</TextLoading>
|
||||
<TextLoading width={120} height={15} loading={loading}>
|
||||
<TextLoading width={120} height={15}>
|
||||
<p className="text-[#616161] text-[14px] font-normal">
|
||||
تخصص: {doctor.expertise}
|
||||
تخصص:
|
||||
{doctor?.specialties?.map(
|
||||
(item, idx) =>
|
||||
`${item.name} ${doctor.specialties.length === idx + 1 ? "" : "|"} `
|
||||
)}
|
||||
</p>
|
||||
</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="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5">
|
||||
<LikeD />
|
||||
<p className="text-[#616161] text-[12px] font-normal">
|
||||
{doctor.satisfaction}%
|
||||
{doctor?.satisfaction}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-1 bg-[#F8F8FF] flex items-center rounded-[4px] gap-0.5">
|
||||
<PointD />
|
||||
<p className="text-[#616161] text-[12px] font-normal">
|
||||
{doctor.point}
|
||||
{doctor?.point}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,36 +79,39 @@ function ItemDoctor({ doctor, loading }) {
|
||||
{/* Hours of work */}
|
||||
<div className="flex my-4 items-center justify-start gap-0.5">
|
||||
<ClockD />
|
||||
<TextLoading width={150} height={15} loading={loading}>
|
||||
<TextLoading width={150} height={15}>
|
||||
<p className="text-[#7E7E7E] text-[12px] font-normal">
|
||||
ساعت کاری: {doctor.hours_of_work}
|
||||
ساعت کاری: {doctor?.hours_of_work}
|
||||
</p>
|
||||
</TextLoading>
|
||||
</div>
|
||||
<CustomLoading loading={loading} width={150} height={20}>
|
||||
<CustomLoading width={150} height={20}>
|
||||
<p
|
||||
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(211,_47,_47,_0.04)] text-[#D32F2F]"
|
||||
}`}
|
||||
>
|
||||
{doctor.free_turn
|
||||
? `اولین نوبت آزاد: ${doctor.free_turn}`
|
||||
{Number(doctor?.active)
|
||||
? `اولین نوبت آزاد: ${moment(doctor?.free_turn * 1000).format(
|
||||
"dddd jD jMMMM [ساعت] HH:mm"
|
||||
)}`
|
||||
: "نوبت ندارد"}
|
||||
</p>
|
||||
</CustomLoading>
|
||||
{/* Arrow */}
|
||||
<Link href={`/doctor/${doctor.id}`}>
|
||||
<Link href={`/doctor/${doctor?.uuid}`}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={`!p-[14px] !absolute !left-4 !bottom-4 !rounded-full !w-fit
|
||||
${!doctor.free_turn || (loading && "!bg-[#D7D7D7]")}
|
||||
`}
|
||||
disabled={!doctor.free_turn || loading}
|
||||
onClick={handleLoading}
|
||||
loading={doctor?.loading}
|
||||
className="!p-[14px] !absolute !left-4 !bottom-4 !rounded-full !w-fit"
|
||||
>
|
||||
<ArrowLeftD />
|
||||
<div className={doctor && doctor.loading ? "opacity-0" : ""}>
|
||||
<ArrowLeftD />
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -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,7 +2,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
function Logo({ isAbsolute }) {
|
||||
function Logo({ isAbsolute, matchedCity }) {
|
||||
return (
|
||||
<Link href="/" className={isAbsolute ? "absolute right-[32px] top-0" : ""}>
|
||||
<div className="flex items-center justify-start gap-[5px]">
|
||||
@@ -14,7 +14,7 @@ function Logo({ isAbsolute }) {
|
||||
alt="logo"
|
||||
/>
|
||||
<p className="text-[#526CAC] text-[14px] lg:text-[16px] font-bold font-kalame">
|
||||
نوبت ۷۲۴
|
||||
{matchedCity?.site_name || "نوبت ۷۲۴"}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -2,12 +2,16 @@ import { styleDefault } from "@/mui";
|
||||
import { Box, Button, Modal } from "@mui/material";
|
||||
import CloseModalD from "@/components/icons/CloseModalD";
|
||||
import { removeToken } from "@/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
function ModalLogout({ open, handleClose }) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const logout = () => {
|
||||
setLoading(true);
|
||||
removeToken();
|
||||
window.location.pathname = '/login'
|
||||
handleClose();
|
||||
router.replace("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -37,9 +41,10 @@ function ModalLogout({ open, handleClose }) {
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
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>
|
||||
@@ -3,7 +3,7 @@ function Pageguide({ list }) {
|
||||
<ul className="flex items-center justify-start gap-1">
|
||||
{list.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center justify-start gap-1">
|
||||
<p
|
||||
<h2
|
||||
className={`text-[16px] font-normal
|
||||
${
|
||||
list.length > idx + 1
|
||||
@@ -13,7 +13,7 @@ function Pageguide({ list }) {
|
||||
`}
|
||||
>
|
||||
{item}
|
||||
</p>
|
||||
</h2>
|
||||
<span
|
||||
className={`
|
||||
text-[#9B9B9B] text-[16px] font-normal
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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 (
|
||||
<PaginationMUI
|
||||
// count={doctors.length / 12} org
|
||||
count={20}
|
||||
count={count}
|
||||
page={page || 1}
|
||||
onChange={setPage && handleChange}
|
||||
color="primary"
|
||||
className={count < 2 && "!hidden"}
|
||||
sx={{
|
||||
"& .mui-8q7g72-MuiButtonBase-root-MuiPaginationItem-root": {
|
||||
color: "#616161",
|
||||
|
||||
@@ -8,7 +8,7 @@ function ProgressDetail({ data }) {
|
||||
{data.label}
|
||||
</p>
|
||||
<LinearProgress
|
||||
value={data.progress}
|
||||
value={data}
|
||||
variant="determinate"
|
||||
className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]"
|
||||
sx={{
|
||||
@@ -19,7 +19,7 @@ function ProgressDetail({ data }) {
|
||||
}}
|
||||
/>
|
||||
<p className="text-[#525252] text-[20px] font-medium">
|
||||
{numberToArStyle(data.progress)}%
|
||||
{numberToArStyle(data)}%
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Slider } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
function ProgressChange({ data }) {
|
||||
function ProgressChange({ data, changeData, parent }) {
|
||||
const [value, setValue] = useState(70);
|
||||
|
||||
return (
|
||||
@@ -10,9 +10,12 @@ function ProgressChange({ data }) {
|
||||
{data.label}
|
||||
</p>
|
||||
<Slider
|
||||
defaultValue={value}
|
||||
defaultValue={data.progress}
|
||||
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={{
|
||||
"& .MuiSlider-rail": {
|
||||
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}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
@@ -7,7 +7,7 @@ function a11yProps(index) {
|
||||
};
|
||||
}
|
||||
|
||||
function Tabs({ value, handleChange, listTab, isSm, style }) {
|
||||
function Tabs({ value, handleChange, listTab, inactives, isSm, style }) {
|
||||
return (
|
||||
<Box className="!border-none !w-full">
|
||||
<TabsMUI
|
||||
@@ -24,14 +24,15 @@ function Tabs({ value, handleChange, listTab, isSm, style }) {
|
||||
<Tab
|
||||
key={idx}
|
||||
label={item}
|
||||
disabled={inactives && inactives.includes(idx)}
|
||||
{...a11yProps(idx)}
|
||||
sx={{
|
||||
"&.MuiButtonBase-root": {
|
||||
width: !isSm && "fit-content !important",
|
||||
minWidth: !isSm && "fit-content !important",
|
||||
width: !isSm ? "fit-content !important" : undefined, // Fix: Replace `false` with `undefined`
|
||||
minWidth: !isSm ? "fit-content !important" : undefined,
|
||||
},
|
||||
"&.MuiButtonBase-root:first-child": {
|
||||
marginLeft: !isSm && "9px !important",
|
||||
marginLeft: !isSm ? "9px !important" : undefined,
|
||||
},
|
||||
}}
|
||||
className={`
|
||||
|
||||
@@ -47,7 +47,7 @@ function TimePickerField({ dataChange, isVacation, disable, timeStart, data }) {
|
||||
const hours = e.$H;
|
||||
const minute = e.$m;
|
||||
const newValue = `${handleTwoLength(hours)}:${handleTwoLength(
|
||||
minute
|
||||
minute,
|
||||
)}`;
|
||||
|
||||
const newData = data;
|
||||
|
||||
@@ -18,7 +18,7 @@ function TimePickerInput({ changeData, handleDisableHours, name, data }) {
|
||||
const hours = e.$H;
|
||||
const minute = e.$m;
|
||||
const newValue = `${handleTwoLength(hours)}:${handleTwoLength(
|
||||
minute
|
||||
minute,
|
||||
)}`;
|
||||
// setValue(newValue);
|
||||
changeData(newValue, name);
|
||||
|
||||
@@ -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;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
function Field({ title, type, multiline, isPlaceHolder }) {
|
||||
function Field({ title, type, updateValue, value, multiline, isPlaceHolder }) {
|
||||
return (
|
||||
<TextField
|
||||
placeholder={isPlaceHolder && title}
|
||||
label={!isPlaceHolder ? title : ""}
|
||||
type={type || "text"}
|
||||
className="!w-full !mx-auto !bg-transparent"
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
sx={{
|
||||
"& .MuiFormLabel-root": {
|
||||
top: "-4px !important",
|
||||
|
||||
+123
-139
@@ -1,184 +1,168 @@
|
||||
import Image from "next/image";
|
||||
import { Button, Skeleton } from "@mui/material";
|
||||
import Field from "./Field";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
|
||||
// Icons
|
||||
import BoldDisLikeComment from "@/components/icons/BoldDisLikeComment";
|
||||
import BoldLikeComment from "@/components/icons/BoldLikeComment";
|
||||
import DisLikeComment from "@/components/icons/DisLikeComment";
|
||||
import ArrowUpComment from "@/components/icons/ArrowUpComment";
|
||||
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({
|
||||
update,
|
||||
setUpdate,
|
||||
isReply,
|
||||
list,
|
||||
setList,
|
||||
data,
|
||||
loading,
|
||||
}) {
|
||||
const handleLike = (lengthLike, data, nameLike, nameIsLike) => {
|
||||
if (nameIsLike === "is_like" && data.is_dislike) {
|
||||
data.is_dislike = false;
|
||||
data.dislike--;
|
||||
} else if (nameIsLike === "is_dislike" && data.is_like) {
|
||||
data.is_like = false;
|
||||
data.like--;
|
||||
function ItemUser({ update, setUpdate, data }) {
|
||||
const [loadingLike, setLoadingLike] = useState(false);
|
||||
const [loadingDislike, setLoadingDislike] = useState(false);
|
||||
|
||||
const SendReq = async (type) => {
|
||||
if (type === "like") setLoadingLike(true);
|
||||
else setLoadingDislike(true);
|
||||
|
||||
const body = {
|
||||
comment_id: data.id,
|
||||
like: type === "like" ? 1 : 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await request.postCommentsLike(body);
|
||||
} 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);
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="w-full">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
{loading ? (
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
className="!min-w-[40px] !min-h-[40px]"
|
||||
/>
|
||||
) : (
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
className="!w-[32px] !h-[32px]"
|
||||
src={data.author?.picture[0]?.url}
|
||||
width={40}
|
||||
height={40}
|
||||
alt="profile"
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-center gap-[8px]">
|
||||
{loading ? (
|
||||
<Skeleton variant="text" className="!min-w-[70px]" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
|
||||
{data.name}
|
||||
</p>
|
||||
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
|
||||
3 روز پیش
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
|
||||
{data.author.real_name}
|
||||
</p>
|
||||
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
|
||||
{timeAgo(data.created)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Skeleton variant="text" className="!w-full !mt-8" />
|
||||
<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>
|
||||
)}
|
||||
<p className="text-[#495057] text-[12px] md:text-[14px]">
|
||||
{data.comment}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-start mt-2 gap-[28px]">
|
||||
{/* لایک */}
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
handleLike(
|
||||
data.is_like ? data.like - 1 : data.like + 1,
|
||||
data,
|
||||
"like",
|
||||
"is_like"
|
||||
)
|
||||
}
|
||||
onClick={() => handleLike(data, "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>
|
||||
{loading ? (
|
||||
<Skeleton variant="text" className="!min-w-[8px]" />
|
||||
) : (
|
||||
<p className="text-[#6C757D] text-[12px] font-normal select-none">
|
||||
{data.like}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[#6C757D] text-[12px] select-none">
|
||||
{data.like_status.like_count || ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* دیسلایک */}
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
handleLike(
|
||||
data.is_dislike ? data.dislike - 1 : data.dislike + 1,
|
||||
data,
|
||||
"dislike",
|
||||
"is_dislike"
|
||||
)
|
||||
}
|
||||
onClick={() => handleLike(data, "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>
|
||||
{loading ? (
|
||||
<Skeleton variant="text" className="!min-w-[8px]" />
|
||||
) : (
|
||||
<p className="text-[#6C757D] text-[12px] font-normal select-none">
|
||||
{data.dislike}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[#6C757D] text-[12px] select-none">
|
||||
{data.like_status.dislike_count || ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="!text-[#495057] !text-[12px] !font-medium"
|
||||
onClick={() => {
|
||||
data.is_open_answer = !data.is_open_answer;
|
||||
setUpdate(!update);
|
||||
if (isUserLoggedIn()) {
|
||||
data.is_open_answer = !data.is_open_answer;
|
||||
setUpdate(!update);
|
||||
} else {
|
||||
alert(
|
||||
"warning",
|
||||
"برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید."
|
||||
);
|
||||
}
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
پاسخ
|
||||
</Button>
|
||||
</div>
|
||||
<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" 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>
|
||||
|
||||
<SendAnswer data={data} />
|
||||
{!!data.replies.length && (
|
||||
<AnswerField data={data} update={update} setUpdate={setUpdate} />
|
||||
)}
|
||||
<ul
|
||||
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>
|
||||
<Replies data={data} update={update} setUpdate={setUpdate} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -2,8 +2,8 @@
|
||||
import { useState } from "react";
|
||||
import ItemUser from "./ItemUser";
|
||||
|
||||
function Comment({ data, loading }) {
|
||||
const [list, setList] = useState(data.comments);
|
||||
function Comment({ data, comments }) {
|
||||
const [list, setList] = useState(data?.comments);
|
||||
const [update, setUpdate] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -12,16 +12,13 @@ function Comment({ data, loading }) {
|
||||
false && "w-full"
|
||||
}`}
|
||||
>
|
||||
{data &&
|
||||
!!data.comments.length &&
|
||||
data.comments.map((item, idx) => (
|
||||
{comments &&
|
||||
comments.length &&
|
||||
comments.map((item, idx) => (
|
||||
<ItemUser
|
||||
key={idx}
|
||||
list={list}
|
||||
data={item}
|
||||
update={update}
|
||||
setList={setList}
|
||||
loading={loading}
|
||||
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",
|
||||
};
|
||||
@@ -1,97 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { DatePicker as DatePickerJalali } from "jalaali-react-date-picker";
|
||||
import { Skeleton } from "@mui/material";
|
||||
import NextIconDatePicker from "./NextIconDatePicker";
|
||||
import PrevIconDatePicker from "./PrevIconDatePicker";
|
||||
import FirstDatePicker from "./date/FirstDatePicker";
|
||||
import SecondDatePicker from "./date/SecondDatePicker";
|
||||
import { dateToTimestamp } from "@/helper";
|
||||
import { request } from "@/services/response";
|
||||
import moment from "moment-jalaali";
|
||||
|
||||
function DatePicker({ date, updateDate }) {
|
||||
const nextIconDatePicker = useRef();
|
||||
const prevIconDatePicker = useRef();
|
||||
function DatePicker({ setDate }) {
|
||||
const nextIconDatePickerF = useRef();
|
||||
const prevIconDatePickerF = useRef();
|
||||
const nextIconDatePickerS = useRef();
|
||||
const prevIconDatePickerS = useRef();
|
||||
|
||||
const [startDate, setStartDate] = useState(null);
|
||||
const [endDate, setEndDate] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [disableDates, setDisableDates] = useState([]);
|
||||
|
||||
const handleStartDateChange = (date) => {
|
||||
const handleStartDateChange = (date, e) => {
|
||||
if (date) {
|
||||
setStartDate(date);
|
||||
setDate(dateToTimestamp(date));
|
||||
setEndDate(null);
|
||||
setStartDate(date);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndDateChange = (date) => {
|
||||
if (date) {
|
||||
setDate(dateToTimestamp(date));
|
||||
setEndDate(date);
|
||||
setStartDate(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDisableDate = (e) => {
|
||||
const today = new Date();
|
||||
return today >= e._d;
|
||||
const date = moment(e).startOf("day");
|
||||
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(() => {
|
||||
if (nextIconDatePicker.current) {
|
||||
nextIconDatePicker.current.click();
|
||||
setLoading(false);
|
||||
request
|
||||
.getAppointmentNotAvailable(47)
|
||||
.then((res) => {
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
setDisableDates(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
});
|
||||
if (nextIconDatePickerS.current) {
|
||||
nextIconDatePickerS.current.click();
|
||||
}
|
||||
}, [nextIconDatePicker]);
|
||||
}, [nextIconDatePickerS]);
|
||||
|
||||
return (
|
||||
<div className="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">
|
||||
<DatePickerJalali
|
||||
value={startDate}
|
||||
className={`w-full ${loading ? "opacity-0" : "opacity-100"}`}
|
||||
timePicker={false}
|
||||
nextIcon={
|
||||
<div className="flex md:hidden lg:flex xl:hidden">
|
||||
<NextIconDatePicker nextIconDatePicker={nextIconDatePicker} />
|
||||
</div>
|
||||
}
|
||||
prevIcon={
|
||||
<PrevIconDatePicker prevIconDatePicker={prevIconDatePicker} />
|
||||
}
|
||||
onChange={handleStartDateChange}
|
||||
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 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 className="datePickerApt flex items-start custom-datepicker justify-evenly w-full">
|
||||
<FirstDatePicker
|
||||
loading={loading}
|
||||
startDate={startDate}
|
||||
disableDates={disableDates}
|
||||
handleDisableDate={handleDisableDate}
|
||||
nextIconDatePickerF={nextIconDatePickerF}
|
||||
prevIconDatePickerF={prevIconDatePickerF}
|
||||
prevIconDatePickerS={prevIconDatePickerS}
|
||||
handleStartDateChange={handleStartDateChange}
|
||||
/>
|
||||
<SecondDatePicker
|
||||
loading={loading}
|
||||
endDate={endDate}
|
||||
handleDisableDate={handleDisableDate}
|
||||
nextIconDatePickerF={nextIconDatePickerF}
|
||||
nextIconDatePickerS={nextIconDatePickerS}
|
||||
prevIconDatePickerS={prevIconDatePickerS}
|
||||
handleEndDateChange={handleEndDateChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,19 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Hours from "./Hours";
|
||||
import { useEffect, useState } from "react";
|
||||
import Hours from "./hours";
|
||||
import Tabs from "../../Tabs";
|
||||
import { Button } from "@mui/material";
|
||||
import ArrowLeftB from "@/components/icons/ArrowLeftB";
|
||||
import { request } from "@/services/response";
|
||||
import { useParams } from "next/navigation";
|
||||
import SendAppo from "./SendAppo";
|
||||
|
||||
const listTab = ["صبح", "بعد از ظهر"];
|
||||
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 [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 (
|
||||
<div className="w-full">
|
||||
@@ -41,29 +66,14 @@ function DateTime({ doctor, setStep, setIsError, locateVisit }) {
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<Hours
|
||||
appo={appo}
|
||||
hour={hour}
|
||||
value={value}
|
||||
doctor={doctor}
|
||||
setHour={setHour}
|
||||
nameHours={nameHours}
|
||||
/>
|
||||
<div className="w-full flex justify-end">
|
||||
<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>
|
||||
<SendAppo hour={hour} setStep={setStep} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,42 @@ import BottomSelect from "@/components/icons/BottomSelect";
|
||||
import { Autocomplete, Button, TextField } from "@mui/material";
|
||||
import { styleTextSelectRight } from "@/mui";
|
||||
|
||||
function AutoCompleteSelect({ list, isError, updateData, name, label }) {
|
||||
function AutoCompleteSelect({
|
||||
list,
|
||||
value,
|
||||
isError,
|
||||
updateData,
|
||||
label,
|
||||
disabled = false,
|
||||
listboxProps,
|
||||
sendAll,
|
||||
name,
|
||||
}) {
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
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"
|
||||
onChange={(event) => {
|
||||
updateData && updateData(event.target.innerText, "value", name);
|
||||
onChange={(e, newValue) => {
|
||||
updateData &&
|
||||
updateData(
|
||||
newValue && sendAll
|
||||
? newValue
|
||||
: (newValue && !sendAll && newValue.label) || "",
|
||||
name
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
...styleTextSelectRight,
|
||||
@@ -51,11 +79,10 @@ function AutoCompleteSelect({ list, isError, updateData, name, label }) {
|
||||
<Button
|
||||
fullWidth
|
||||
{...props}
|
||||
style={{}}
|
||||
key={props.id}
|
||||
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.label}
|
||||
{option.name || option.label}
|
||||
</Button>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
|
||||
@@ -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;
|
||||
@@ -3,17 +3,30 @@ import { styleTextSelectRight } from "@/mui";
|
||||
|
||||
function Field({
|
||||
title,
|
||||
error,
|
||||
placeholder,
|
||||
inputProps,
|
||||
type,
|
||||
dir,
|
||||
// isPlaceHolder
|
||||
value,
|
||||
updateState,
|
||||
}) {
|
||||
return (
|
||||
<TextField
|
||||
label={title}
|
||||
error={error}
|
||||
dir={dir || "rtl"}
|
||||
type={type || "text"}
|
||||
value={value}
|
||||
onChange={(e) => updateState(e.target.value)}
|
||||
className="!w-full !mx-auto !bg-[#FFF]"
|
||||
InputProps={inputProps}
|
||||
placeholder={placeholder || ""}
|
||||
sx={{
|
||||
".MuiOutlinedInput-notchedOutline.muirtl-1d3z3hw-MuiOutlinedInput-notchedOutline":
|
||||
{
|
||||
border: error && "1px solid red !important",
|
||||
},
|
||||
...styleTextSelectRight,
|
||||
// Hide Icon Number
|
||||
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
|
||||
@@ -23,8 +36,10 @@ function Field({
|
||||
"& input[type=number]": {
|
||||
MozAppearance: "textfield",
|
||||
},
|
||||
// "& .MuiFormLabel-root": { top: "-7px !important" },
|
||||
"& .MuiInputBase-input": { padding: "12.5px 14px" },
|
||||
"& .MuiInputBase-input": {
|
||||
padding: "12.5px 14px",
|
||||
color: "#0009",
|
||||
},
|
||||
"& .MuiInputBase-root": { borderRadius: "6px !important" },
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "1px solid #E9ECEF !important",
|
||||
|
||||
@@ -20,9 +20,9 @@ function EditField({
|
||||
<TextField
|
||||
{...props}
|
||||
rows={multiline}
|
||||
value={data?.value}
|
||||
multiline={multiline}
|
||||
type={type || "text"}
|
||||
value={data?.value || ""}
|
||||
placeholder={placeholder || ""}
|
||||
disabled={noDisable ? false : !data?.isEdit}
|
||||
className={`!w-full res-field-account dark:!bg-transparent ${
|
||||
@@ -1,16 +1,29 @@
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
function Field({ title, type, props, multiline, isPlaceHolder }) {
|
||||
function Field({
|
||||
type,
|
||||
name,
|
||||
title,
|
||||
props,
|
||||
value,
|
||||
multiline,
|
||||
handleChange,
|
||||
isPlaceHolder,
|
||||
}) {
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
size="small"
|
||||
placeholder={isPlaceHolder && title}
|
||||
label={!isPlaceHolder ? title : ""}
|
||||
defaultValue={value || ""}
|
||||
type={type || "text"}
|
||||
multiline={!!multiline}
|
||||
rows={!!multiline ? 4 : 1}
|
||||
label={!isPlaceHolder ? title : ""}
|
||||
placeholder={isPlaceHolder && title}
|
||||
className="!w-full !mx-auto !bg-[#FFF]"
|
||||
onChange={(e) => {
|
||||
handleChange(name || "", e.target.value);
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiFormLabel-root": {
|
||||
top: "-4px !important",
|
||||
@@ -3,14 +3,14 @@ function HeadPageList({ children, title, detail }) {
|
||||
<div className="bg-head-menu !bg-no-repeat !bg-cover !bg-center">
|
||||
<div
|
||||
className="
|
||||
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)]
|
||||
pb-[62px] sm:pb-[69px] md:pb-[76px] lg:pb-[85px]
|
||||
"
|
||||
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)]
|
||||
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}
|
||||
</p>
|
||||
</h1>
|
||||
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] text-center font-normal">
|
||||
{detail}
|
||||
</p>
|
||||
|
||||
@@ -4,12 +4,13 @@ function MultilineLoading({ loading, width, height, children, line }) {
|
||||
return loading
|
||||
? Array(line)
|
||||
.fill({})
|
||||
.map((_) => (
|
||||
.map((_, idx) => (
|
||||
<Skeleton
|
||||
className={`${width === "full" ? "!w-full" : ""} !my-2`}
|
||||
variant="rounded"
|
||||
height={height}
|
||||
width={width}
|
||||
key={idx}
|
||||
/>
|
||||
))
|
||||
: 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,39 +1,70 @@
|
||||
import CloseModalD from "@/components/icons/CloseModalD";
|
||||
import Image from "next/image";
|
||||
|
||||
const maps = [
|
||||
{ src: "/assets/images/snapp.png", name: "snapp" },
|
||||
{ src: "/assets/images/tapsi.png", name: "tapsi" },
|
||||
{ src: "/assets/images/maps.png", name: "maps" },
|
||||
{ src: "/assets/images/balad.png", name: "balad" },
|
||||
{ src: "/assets/images/waze.png", name: "waze" },
|
||||
];
|
||||
function Content({ data, onClose }) {
|
||||
const { latitude, longitude } = data && data.map;
|
||||
|
||||
const maps = [
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<div className="cursor-pointer" onClick={handleClose}>
|
||||
<div className="cursor-pointer" onClick={onClose}>
|
||||
<CloseModalD />
|
||||
</div>
|
||||
</div>
|
||||
<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]">
|
||||
{maps.map((item, idx) => (
|
||||
<li className="flex items-center flex-col justify-center gap-1">
|
||||
<Image
|
||||
className="w-[32px] h-[32px] lg:w-[40px] lg:h-[40px]"
|
||||
src={item.src}
|
||||
alt="icon app"
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<p className="text-[#3B3B3B] font-normal text-[14px]">
|
||||
{item.name}
|
||||
</p>
|
||||
<li
|
||||
key={idx}
|
||||
className="flex items-center flex-col justify-center gap-1"
|
||||
>
|
||||
<a href={item.url} target="_blank" rel="noopener noreferrer">
|
||||
<Image
|
||||
className="
|
||||
w-[32px] h-[32px] lg:w-[40px] lg:h-[40px]
|
||||
min-w-[32px] min-h-[32px] lg:min-w-[40px] lg:min-h-[40px]
|
||||
max-w-[32px] max-h-[32px] lg:max-w-[40px] lg:max-h-[40px]
|
||||
"
|
||||
src={item.src}
|
||||
alt="icon app"
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<p className="text-[#3B3B3B] font-normal text-[14px]">
|
||||
{item.name}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Drawer, Modal } from "@mui/material";
|
||||
import { styleDefault } from "@/mui";
|
||||
import Content from "./Content";
|
||||
|
||||
function ModalOpenLocation({ open, setOpen, handleClose, children }) {
|
||||
function ModalOpenLocation({ open, data, setOpen, handleClose, children }) {
|
||||
const toggleDrawer = (newOpen) => () => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ function ModalOpenLocation({ open, setOpen, handleClose, children }) {
|
||||
style={styleDefault}
|
||||
className="!pt-[12px] !pb-[28px] !px-[24px] !bg-[#FAFAFA] !rounded-[8px] !overflow-hidden"
|
||||
>
|
||||
<Content onClose={handleClose} />
|
||||
<Content data={data} onClose={handleClose} />
|
||||
</div>
|
||||
</Modal>
|
||||
<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)]">
|
||||
<Content handleClose={handleClose} />
|
||||
<Content data={data} handleClose={handleClose} />
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Button } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import Cropper from "react-easy-crop";
|
||||
import { cropImage } from "./cropImg";
|
||||
import { cropImage, dataURLtoFile } from "./cropImg";
|
||||
|
||||
function Crop({ data, uploadImg, handleClose }) {
|
||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
|
||||
|
||||
const onComplete = (imagePromisse) =>
|
||||
imagePromisse.then((image) => uploadImg(image));
|
||||
const onComplete = async (imagePromise, name) => {
|
||||
const dataUrl = await imagePromise;
|
||||
|
||||
const file = dataURLtoFile(dataUrl, name);
|
||||
|
||||
uploadImg({ blob: dataUrl, file });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-[16px]">
|
||||
@@ -18,7 +23,7 @@ function Crop({ data, uploadImg, handleClose }) {
|
||||
<Cropper
|
||||
crop={crop}
|
||||
zoom={zoom}
|
||||
aspect={4 / 3}
|
||||
aspect={1}
|
||||
image={data.blob}
|
||||
onCropChange={setCrop}
|
||||
onCropComplete={(_, croppedAreaPixels) =>
|
||||
@@ -38,7 +43,9 @@ function Crop({ data, uploadImg, handleClose }) {
|
||||
</Button>
|
||||
<Button
|
||||
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]"
|
||||
>
|
||||
ذخیره
|
||||
|
||||
@@ -37,6 +37,20 @@ async function getCroppedImg(imageSrc, pixelCrop) {
|
||||
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) => {
|
||||
try {
|
||||
const croppedImage = await getCroppedImg(image, croppedAreaPixels);
|
||||
|
||||
@@ -54,7 +54,7 @@ function UploadFile({ image, setImage }) {
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
src={image}
|
||||
src={image.blob}
|
||||
alt="img-uploaded"
|
||||
className="w-[74px] h-[74px] rounded-full overflow-hidden object-cover"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import ContactUsPage from "@/components/contactUs";
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
|
||||
function ContactUs() {
|
||||
return (
|
||||
<Layout title="تماس با ما" name="contact-us">
|
||||
<Layout name="/contact-us">
|
||||
<ContactUsPage />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
+22
-30
@@ -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";
|
||||
import DashboardPage from "@/components/dashboard";
|
||||
import UserAccountPage from "@/components/dashboard/userAccount/UserAccount";
|
||||
const ability = defineAbilitiesFor(user);
|
||||
const cookieStore = cookies();
|
||||
const token = cookieStore.get("access_token");
|
||||
|
||||
function UserAccount() {
|
||||
const [value, setValue] = useState(0);
|
||||
const [isOpenSide, setIsOpenSide] = useState(false);
|
||||
const [isTurnsDetails, setIsTurnsDetails] = useState(false);
|
||||
if (!ability.can("access", "Dashboard")) {
|
||||
removeToken();
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardPage
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
user={userData.data}
|
||||
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>
|
||||
<Content
|
||||
logged={token.value}
|
||||
params={searchParams}
|
||||
matchedCity={matchedCity}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAccount;
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
import DoctorPage from "@/components/doctor";
|
||||
import doctors from "@/data/doctors.json";
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import axios from "axios";
|
||||
|
||||
function Doctor({ params: { slug } }) {
|
||||
const doctor = doctors.find((item) => item.id === +slug);
|
||||
async function Doctor({ params: { 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 (
|
||||
<Layout title="دکتر ها">
|
||||
<DoctorPage doctor={doctor} />
|
||||
<Layout>
|
||||
<DoctorPage doctor={doctor} doctors={doctors} comments={comments} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+42
-4
@@ -1,10 +1,48 @@
|
||||
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 (
|
||||
<Layout title="دکتر ها">
|
||||
<DoctorsPage />
|
||||
<Layout>
|
||||
<DoctorsPage
|
||||
list={doctors}
|
||||
params={searchParams}
|
||||
matchedCity={matchedCity}
|
||||
matchedState={matchedState}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+158
-62
@@ -2,62 +2,6 @@
|
||||
@tailwind components;
|
||||
@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-face {
|
||||
@@ -114,8 +58,64 @@
|
||||
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 {
|
||||
font-family: iran-sans;
|
||||
font-family: vazir;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
@@ -237,7 +237,7 @@ html {
|
||||
}
|
||||
|
||||
.MuiFormControl-root .MuiInputBase-root .MuiInputBase-input {
|
||||
font-size: 16px !important ;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@keyframes openx {
|
||||
@@ -295,7 +295,7 @@ html {
|
||||
}
|
||||
|
||||
.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_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,
|
||||
@@ -451,7 +458,8 @@ html {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon-chevron-left::before {
|
||||
.icon-chevron-left::before,
|
||||
.icon-keyboard_double_arrow_left::before {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
@@ -706,6 +714,10 @@ html {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.MuiPaper-root .MuiList-root .MuiListSubheader-root {
|
||||
background: #5559ce;
|
||||
}
|
||||
|
||||
.panel-jalaali {
|
||||
width: 464px !important;
|
||||
}
|
||||
@@ -792,7 +804,7 @@ html {
|
||||
|
||||
.Toastify__toast {
|
||||
padding: 12px 40px 12px 12px !important;
|
||||
font-family: iran-sans !important;
|
||||
font-family: vazir !important;
|
||||
}
|
||||
|
||||
.Toastify__toast .Toastify__toast-body {
|
||||
@@ -846,3 +858,87 @@ html {
|
||||
}
|
||||
|
||||
/* 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
@@ -4,31 +4,43 @@ import "jalaali-react-date-picker/lib/styles/index.css";
|
||||
import { Providers } from "./Providers";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import { GoogleTagManager } from "@next/third-parties/google";
|
||||
|
||||
// Toastify
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import CustomToastify from "./CustomToastify";
|
||||
import { ProvinceProvider } from "@/context/ProvinceProvider";
|
||||
import { getStateInfo } from "@/lib/getStateInfo";
|
||||
|
||||
export const metadata = {
|
||||
title: "نوبت724",
|
||||
description:
|
||||
"نوبت 724 - سیستم آنلاین نوبتدهی برای پزشکان و کلینیکها. با استفاده از نوبت 724، به راحتی نوبت پزشکی خود را به صورت آنلاین رزرو کنید و از خدمات سریع و کارآمد ما بهرهمند شوید",
|
||||
keywords:
|
||||
"نوبت 724, نوبت دهی, رزرو نوبت, پزشک, پزشکی, کلینیک, بیمارستان, نوبت آنلاین, سیستم نوبت دهی, سیستم نوبت دهی آنلاین, سیستم نوبت دهی پزشکی, سیستم نوبت دهی کلینیک, سیستم نوبت دهی بیمارستان, سیستم نوبت دهی آنلاین پزشکی, سیستم نوبت دهی آنلاین کلینیک, سیستم نوبت دهی آنلاین بیمارستان",
|
||||
image: "https://www.nobat724.com/assets/images/logo.png",
|
||||
};
|
||||
export async function generateMetadata() {
|
||||
const { matchedCity } = getStateInfo();
|
||||
|
||||
return {
|
||||
title: matchedCity ? matchedCity.title : "نوبت724",
|
||||
description: matchedCity
|
||||
? matchedCity.description
|
||||
: "نوبت 724 - سیستم آنلاین نوبتدهی برای پزشکان و کلینیکها. با استفاده از نوبت 724، به راحتی نوبت پزشکی خود را به صورت آنلاین رزرو کنید و از خدمات سریع و کارآمد ما بهرهمند شوید",
|
||||
keywords: matchedCity
|
||||
? matchedCity.keywords
|
||||
: "نوبت 724, نوبت دهی, رزرو نوبت, پزشک, پزشکی, کلینیک, بیمارستان, نوبت آنلاین, سیستم نوبت دهی, سیستم نوبت دهی آنلاین, سیستم نوبت دهی پزشکی, سیستم نوبت دهی کلینیک, سیستم نوبت دهی بیمارستان, سیستم نوبت دهی آنلاین پزشکی, سیستم نوبت دهی آنلاین کلینیک, سیستم نوبت دهی آنلاین بیمارستان",
|
||||
openGraph: {
|
||||
images: ["https://www.nobat724.com/assets/images/logo.png"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="fa" dir="rtl" suppressHydrationWarning>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1"
|
||||
/>
|
||||
<head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1"
|
||||
/>
|
||||
</head>
|
||||
<ThemeRegistry>
|
||||
<GoogleTagManager gtmId="GTM-NXBSV7GS" />
|
||||
<body className="bg-[#FAFAFA]">
|
||||
<Providers>{children}</Providers>
|
||||
<ProvinceProvider>
|
||||
<Providers>{children}</Providers>
|
||||
</ProvinceProvider>
|
||||
<CustomToastify />
|
||||
</body>
|
||||
<GoogleAnalytics gaId="G-HG3RZ1PJS5" />
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import RegisterPage from "@/components/register";
|
||||
import SetCodePage from "@/components/register/SetCodePage";
|
||||
import LogInPage from "@/components/register/LogInPage";
|
||||
import { useState } from "react";
|
||||
import ContentVerify from "@/components/register/ContentVerify";
|
||||
|
||||
function LoginVerify() {
|
||||
const [isSendMsg, setIsSendMsg] = useState(false);
|
||||
|
||||
return (
|
||||
<RegisterPage>
|
||||
{isSendMsg ? (
|
||||
<SetCodePage
|
||||
setIsSendMsg={setIsSendMsg}
|
||||
link={`/account/${localStorage.getItem("doctor-id")}`}
|
||||
/>
|
||||
) : (
|
||||
<LogInPage setIsSendMsg={setIsSendMsg} />
|
||||
)}
|
||||
</RegisterPage>
|
||||
);
|
||||
return <ContentVerify />;
|
||||
}
|
||||
|
||||
export default LoginVerify;
|
||||
|
||||
+12
-18
@@ -1,23 +1,17 @@
|
||||
"use client";
|
||||
import RegisterPage from "@/components/register";
|
||||
import SetCodePage from "@/components/register/SetCodePage";
|
||||
import LogInPage from "@/components/register/LogInPage";
|
||||
import { useState } from "react";
|
||||
import ContentLogin from "@/components/register/ContentLogin";
|
||||
import { defineAbilitiesFor } from "@/lib/ability";
|
||||
import { getUser } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
function LogIn() {
|
||||
const [isSendMsg, setIsSendMsg] = useState(false);
|
||||
async function LogIn() {
|
||||
const user = await getUser();
|
||||
const ability = defineAbilitiesFor(user);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RegisterPage>
|
||||
{isSendMsg ? (
|
||||
<SetCodePage setIsSendMsg={setIsSendMsg} setStep={false} link="/" />
|
||||
) : (
|
||||
<LogInPage setStep={false} setIsSendMsg={setIsSendMsg} />
|
||||
)}
|
||||
</RegisterPage>
|
||||
</div>
|
||||
);
|
||||
if (!ability.can("access", "Login")) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
return <ContentLogin />;
|
||||
}
|
||||
|
||||
export default LogIn;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import NotFoundPage from "@/components/notFound";
|
||||
|
||||
function NotFound() {
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import HomePage from "@/components/home";
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout title="خانه" name="">
|
||||
<Layout name="/">
|
||||
<HomePage />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import DashboardPage from "@/components/panel/dashboard";
|
||||
import Information from "@/data/information.json";
|
||||
|
||||
|
||||
@@ -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";
|
||||
import Header from "@/components/layoutPanel/header";
|
||||
import Sidebar from "@/components/layoutPanel/sidebar";
|
||||
import Information from "@/data/information.json";
|
||||
import UserDetail from "@/components/layoutPanel/userDetail";
|
||||
import { usePathname } from "next/navigation";
|
||||
import BgGray from "@/components/layoutPanel/sidebar/BgGray";
|
||||
async function LayoutPanel({ children }) {
|
||||
const user = await getUser();
|
||||
const ability = defineAbilitiesFor(user);
|
||||
|
||||
function LayoutPanel({ children }) {
|
||||
const pathname = usePathname();
|
||||
const [active, setActive] = useState(false);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
if (!ability.can("access", "Panel")) {
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
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>
|
||||
);
|
||||
return <Content children={children} />;
|
||||
}
|
||||
|
||||
export default LayoutPanel;
|
||||
|
||||
@@ -3,7 +3,7 @@ import Information from "@/data/information.json";
|
||||
import Bank from "@/data/bank.json";
|
||||
|
||||
function UserAccount() {
|
||||
return <UserAccountPage data={{information: Information, bank: Bank}} />;
|
||||
return <UserAccountPage data={{ information: Information, bank: Bank }} />;
|
||||
}
|
||||
|
||||
export default UserAccount;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Layout from "@/components/layout";
|
||||
import Layout from "@/components/layout/StLayout";
|
||||
import SpecialtiesPage from "@/components/specialties";
|
||||
|
||||
function Specialties() {
|
||||
return (
|
||||
<Layout title="تخصص ها" name="specialties">
|
||||
<Layout name="/specialties">
|
||||
<SpecialtiesPage />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { createTheme } from "@mui/material/styles";
|
||||
const theme = createTheme({
|
||||
direction: "rtl",
|
||||
typography: {
|
||||
fontFamily: "iran-sans",
|
||||
fontFamily: "vazir",
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { getStateInfo } from "@/lib/getStateInfo";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
function Head() {
|
||||
const { matchedCity } = getStateInfo();
|
||||
|
||||
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]">
|
||||
<Image
|
||||
@@ -10,9 +13,9 @@ function Head() {
|
||||
width={46}
|
||||
alt="logo"
|
||||
/>
|
||||
<p className="text-[#FAFAFA] text-[16px] sm:text-[18px] md:text-[22px] lg:text-[24px] font-bold">
|
||||
درباره نوبت ۷۲۴
|
||||
</p>
|
||||
<h1 className="text-[#FAFAFA] text-[16px] sm:text-[18px] md:text-[22px] lg:text-[24px] font-bold">
|
||||
درباره {matchedCity?.site_name}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ function Services() {
|
||||
return (
|
||||
<div className="mt-[16px] sm:mt-[24px] md:mt-[32px] lg:mt-[40px] px-[16px] sm:px-[12%]">
|
||||
<div className="flex justify-center">
|
||||
<AnimationTextHead name="zoom-in-up" text="خدمات ما">
|
||||
<AnimationTextHead text="خدمات ما">
|
||||
<UnderlineLG />
|
||||
</AnimationTextHead>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ function AboutUsPage() {
|
||||
"
|
||||
>
|
||||
<Head />
|
||||
<p
|
||||
<h2
|
||||
className="
|
||||
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
|
||||
@@ -26,7 +26,7 @@ function AboutUsPage() {
|
||||
داشت که تمام و دشواری موجود در ارائه راهکارها، و شرایط سخت تایپ به پایان
|
||||
رسد و زمان مورد نیاز شامل حروفچینی دستاوردهای اصلی، و جوابگوی سوالات
|
||||
پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد.
|
||||
</p>
|
||||
</h2>
|
||||
<Services />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 Location from "./location";
|
||||
|
||||
function BookingPage({
|
||||
doctor,
|
||||
step,
|
||||
setStep,
|
||||
children,
|
||||
isFirst,
|
||||
disableSide,
|
||||
}) {
|
||||
function Content({ doctor, step, setStep, children, isFirst, disableSide }) {
|
||||
return (
|
||||
<div
|
||||
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;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import DatePicker from "@/app/component/date/datePicker";
|
||||
|
||||
function SelectDatePicker() {
|
||||
return <DatePicker />;
|
||||
function SelectDatePicker({ setDate }) {
|
||||
return <DatePicker setDate={setDate} />;
|
||||
}
|
||||
|
||||
export default SelectDatePicker;
|
||||
@@ -1,12 +1,12 @@
|
||||
import SelectDatePicker from "./SelectDatePicker";
|
||||
|
||||
function Time({ isStep }) {
|
||||
function Time({ isStep, setDate }) {
|
||||
return (
|
||||
<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">
|
||||
2. انتخاب روز
|
||||
1. انتخاب روز
|
||||
</p>
|
||||
<SelectDatePicker />
|
||||
<SelectDatePicker setDate={setDate} />
|
||||
{!isStep && (
|
||||
<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";
|
||||
|
||||
function Hour({ doctor, setStep, setIsError, locateVisit, isStep }) {
|
||||
function Hour({ doctor, setStep, date, locateVisit, isStep }) {
|
||||
return (
|
||||
<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">
|
||||
3. انتخاب ساعت
|
||||
2. انتخاب ساعت
|
||||
</p>
|
||||
<DateTime
|
||||
date={date}
|
||||
doctor={doctor}
|
||||
setStep={setStep}
|
||||
setIsError={setIsError}
|
||||
locateVisit={locateVisit}
|
||||
/>
|
||||
{!isStep && (
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Hour from "./hour";
|
||||
import Time from "./Time";
|
||||
import PlaceVisit from "./PlaceVisit";
|
||||
|
||||
function Date({ doctor, setStep }) {
|
||||
const [locateVisit, setLocateVisit] = useState(true);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [date, setDate] = useState();
|
||||
|
||||
return (
|
||||
<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]
|
||||
bg-transparent lg:bg-[#FFF]"
|
||||
>
|
||||
<PlaceVisit
|
||||
setLocateVisit={setLocateVisit}
|
||||
locateVisit={locateVisit}
|
||||
isError={isError}
|
||||
/>
|
||||
<Time
|
||||
isStep={doctor.multiwork ? locateVisit : true}
|
||||
setDate={setDate}
|
||||
locateVisit={locateVisit}
|
||||
isStep={doctor?.multiwork ? locateVisit : true}
|
||||
/>
|
||||
<Hour
|
||||
isStep={doctor.multiwork ? locateVisit : true}
|
||||
isStep={doctor?.multiwork ? locateVisit : true}
|
||||
setLocateVisit={setLocateVisit}
|
||||
locateVisit={locateVisit}
|
||||
setIsError={setIsError}
|
||||
setStep={setStep}
|
||||
doctor={doctor}
|
||||
date={date}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,16 @@
|
||||
import AutoCompleteSelect from "@/app/component/element/AutoCompleteSelect";
|
||||
import EditField from "../../../app/component/EditField";
|
||||
import Content from "./Content";
|
||||
import EditField from "../../../app/component/fields/EditField";
|
||||
import DefaultSelect from "@/app/component/selectors/DefaultSelect";
|
||||
|
||||
const bime = [
|
||||
{ label: "بیمه 1", id: 10 },
|
||||
{ label: "بیمه 2", id: 20 },
|
||||
{ label: "بیمه 3", id: 30 },
|
||||
];
|
||||
function Form({ changeData, insurance, data, isForAnother }) {
|
||||
const findVal = () => {
|
||||
return insurance?.find(
|
||||
(g) =>
|
||||
g.id ===
|
||||
(data?.basic_insurance?.value ? data?.basic_insurance.value.id : false)
|
||||
);
|
||||
};
|
||||
|
||||
function Form({ changeData, data, updateSelect, isForAnother }) {
|
||||
return (
|
||||
<div
|
||||
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="کد ملی">
|
||||
<EditField
|
||||
changeData={changeData}
|
||||
data={data?.codemeli}
|
||||
name="codemeli"
|
||||
data={data?.national_code}
|
||||
name="national_code"
|
||||
/>
|
||||
</Content>
|
||||
<Content title="نام و نام خانوادگی">
|
||||
<EditField changeData={changeData} data={data?.name} name="name" />
|
||||
</Content>
|
||||
<Content title="نوع بیمه">
|
||||
<AutoCompleteSelect
|
||||
updateData={updateSelect}
|
||||
<DefaultSelect
|
||||
updateData={(name, val) => changeData(val, "value", name)}
|
||||
label="نوع بیمه"
|
||||
name="first"
|
||||
list={bime}
|
||||
name="basic_insurance"
|
||||
list={insurance}
|
||||
value={findVal()}
|
||||
/>
|
||||
</Content>
|
||||
</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 AddCircleBlueA from "@/components/icons/AddCircleBlueA";
|
||||
import ArrowLeftB from "@/components/icons/ArrowLeftB";
|
||||
import ButtonFixed from "../paying/ButtonFixed";
|
||||
import Form from "./Form";
|
||||
import { request } from "@/services/response";
|
||||
import SubmitData from "./SubmitData";
|
||||
|
||||
function Detail({
|
||||
isForAnother,
|
||||
@@ -11,14 +11,11 @@ function Detail({
|
||||
setIsForAnother,
|
||||
data,
|
||||
setData,
|
||||
prevData,
|
||||
fadeElement,
|
||||
setIsPay,
|
||||
}) {
|
||||
const [selected, setSelected] = useState({
|
||||
first: "",
|
||||
second: "",
|
||||
});
|
||||
|
||||
const [insurance, setInsurance] = useState();
|
||||
const changeData = (value, type, name) =>
|
||||
setData({
|
||||
...data,
|
||||
@@ -28,8 +25,13 @@ function Detail({
|
||||
},
|
||||
});
|
||||
|
||||
const updateSelect = (event, name) =>
|
||||
setSelected({ ...selected, [name]: event });
|
||||
useEffect(() => {
|
||||
request.getInsuranceType().then((res) => {
|
||||
if (res && res.data) {
|
||||
setInsurance(res.data);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// ${typePay === 'success' || typePay === 'failed' ? 'bg-transparent border-transparent md:bg-[#FFF] md:border-[#EFEFEF]' : ''}
|
||||
@@ -43,10 +45,10 @@ function Detail({
|
||||
{isForAnother ? "اطلاعات کاربر جدید" : "اطلاعات حساب کاربری"}
|
||||
</p>
|
||||
<Form
|
||||
isForAnother={isForAnother}
|
||||
updateSelect={updateSelect}
|
||||
changeData={changeData}
|
||||
data={data}
|
||||
insurance={insurance}
|
||||
changeData={changeData}
|
||||
isForAnother={isForAnother}
|
||||
/>
|
||||
{!isForAnother && (
|
||||
<Button
|
||||
@@ -66,25 +68,7 @@ function Detail({
|
||||
</p>
|
||||
</Button>
|
||||
)}
|
||||
<ButtonFixed>
|
||||
<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>
|
||||
<SubmitData setStep={setStep} data={data} prevData={prevData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -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;
|
||||
+1
-1
@@ -14,7 +14,7 @@ function Detail({ doctor, step, setStep }) {
|
||||
<LocationA />
|
||||
</div>
|
||||
<p className="text-[#616161] text-[14px] md:text-[16px] font-normal">
|
||||
آدرس: {doctor.location}
|
||||
آدرس: {doctor?.location}
|
||||
</p>
|
||||
</li>
|
||||
<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]
|
||||
"
|
||||
alt="profile doctor"
|
||||
src={doctor.img}
|
||||
src={doctor?.img}
|
||||
height={95}
|
||||
width={95}
|
||||
/>
|
||||
<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">
|
||||
{doctor.name}
|
||||
{doctor?.name}
|
||||
</p>
|
||||
<p className="text-[#616161] text-[14px] md:text-[16px] font-medium">
|
||||
تخصص: {doctor.expertise}
|
||||
تخصص: {doctor?.expertise}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
+3
-1
@@ -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} />
|
||||
<Detail doctor={doctor} step={step} setStep={setStep} />
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
function Address({ doctor }) {
|
||||
return (
|
||||
<ul className="hidden mt-[4px] lg:flex flex-col">
|
||||
{doctor.address.map((item, idx) => (
|
||||
{doctor?.address?.map((item, idx) => (
|
||||
<li key={idx}>
|
||||
<div className="flex py-[12px] text-[#616161] text-[14px] items-start justify-start">
|
||||
<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]
|
||||
h-[48px] sm:h-[51px] md:h-[54px] lg:h-[56px]
|
||||
"
|
||||
src={doctor.img}
|
||||
src={doctor?.img}
|
||||
alt="profile"
|
||||
height={56}
|
||||
width={56}
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start gap-[8px]">
|
||||
<p className="text-[#3B3B3B] text-[12px] md:text-[14px] font-bold">
|
||||
{doctor.name}
|
||||
</p>
|
||||
<p className="text-[#525252] text-[12px] md:text-[14px] font-medium">
|
||||
تخصص: {doctor.expertise}
|
||||
</p>
|
||||
<h2 className="text-[#3B3B3B] text-[12px] md:text-[14px] font-bold">
|
||||
{doctor?.name}
|
||||
</h2>
|
||||
<h3 className="text-[#525252] text-[12px] md:text-[14px] font-medium">
|
||||
تخصص: {doctor?.expertise}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user