mearge dev

This commit is contained in:
2025-09-23 11:24:11 +03:30
375 changed files with 11529 additions and 37562 deletions
+3
View File
@@ -34,3 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.idx/dev.nix
.vscode/settings.json
.env
+48
View File
@@ -0,0 +1,48 @@
# Dockerfile for Next.js 14 - Optimized for Production
# Use the official Node.js image with Alpine Linux for a smaller footprint
FROM node:22-alpine AS base
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json (or yarn.lock)
COPY package*.json ./
# Install dependencies - leveraging Docker cache
RUN npm i --force
# Copy the rest of the application code
COPY . .
# Build the Next.js application
RUN npm run build
# Stage 2: Production image - smaller and leaner
FROM node:22-alpine AS runner
# Set the working directory
WORKDIR /app
# Set environment variables
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# Add a non-root user for security
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Copy only the necessary files from the builder stage
COPY --from=base /app/next.config.js ./
COPY --from=base /app/public ./public
COPY --from=base --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=base --chown=nextjs:nodejs /app/.next/static ./.next/static
# Change ownership of all copied files to the non-root user
USER nextjs
# Expose the port Next.js listens on
EXPOSE 3000
# Command to start the Next.js server in production mode
CMD ["node", "server.js"]
+10 -2
View File
@@ -29,8 +29,6 @@ npm install
## 🛠 Available Scripts
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
+2 -2
View File
@@ -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>
);
+21
View File
@@ -0,0 +1,21 @@
import AppointmentPage from "@/components/appointment";
import { getStateInfo } from "@/lib/getStateInfo";
import axios from "axios";
async function Appointment({ params: { slug } }) {
const { matchedCity } = getStateInfo();
const API_URL = process.env.NEXT_PUBLIC_API_URL;
let apt = null;
try {
const response = await axios.get(`${API_URL}/api/v1/appointment-slots`);
apt = response.data.data;
} catch (error) {
// console.error("problem with req:", error.message);
}
return <AppointmentPage slug={slug} matchedCity={matchedCity} />;
}
export default Appointment;
+9 -6
View File
@@ -1,13 +1,16 @@
import BlogPage from "@/components/blog";
import 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
View File
@@ -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;
-71
View File
@@ -1,71 +0,0 @@
"use client";
import { useState } from "react";
import Layout from "@/components/layout";
import doctors from "@/data/doctors.json";
import Date from "@/components/booking/date";
import BookingPage from "@/components/booking";
import RegisterPage from "@/components/register";
import Paying from "@/components/booking/paying";
import Detail from "@/components/booking/detail";
import FailedPay from "@/components/booking/failedPay";
import LogInPage from "@/components/register/LogInPage";
import SuccessPay from "@/components/booking/successPay";
import SetCodePage from "@/components/register/SetCodePage";
function Booking({ params: { slug } }) {
const doctor = doctors.find((item) => item.id === +slug);
const [step, setStep] = useState(0);
const [isForAnother, setIsForAnother] = useState(false);
const [data, setData] = useState({
phone: { value: "09121056987", isEdit: true },
codemeli: { value: "1741025645", isEdit: true },
name: { value: "ساغر صابری نژاد", isEdit: true },
});
const elements = [
<Date doctor={doctor} setStep={setStep} />,
<LogInPage setIsSendMsg={false} setStep={setStep} />,
<SetCodePage setIsSendMsg={false} link={false} setStep={setStep} />,
<Detail
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
setStep={setStep}
data={data}
setData={setData}
/>,
<Paying setStep={setStep} />,
<SuccessPay setStep={setStep} />,
<FailedPay setStep={setStep} />,
];
return (
<>
{step === 1 || step === 2 ? (
<RegisterPage>{elements[step]}</RegisterPage>
) : (
<Layout
title="رزرو"
name="booking"
disableFooter={step > 2}
paddingBottom="pb-[80px]"
>
<BookingPage
disableSide={step === 5 || step === 6}
isFirst={step === 0}
setStep={setStep}
doctor={doctor}
step={step}
>
{elements[step]}
</BookingPage>
</Layout>
)}
</>
);
}
export default Booking;
+17 -7
View File
@@ -1,16 +1,26 @@
import ClinicPage from "@/components/clinic";
import 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
View File
@@ -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;
+2 -5
View File
@@ -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>
+82
View File
@@ -0,0 +1,82 @@
import { useState } from "react";
import ProfilePA from "@/components/icons/ProfilePA";
import LogoutP from "@/components/icons/LogoutP";
import CardDA from "@/components/icons/CardDA";
import { Button, Popover } from "@mui/material";
import Link from "next/link";
import ModalLogout from "./ModalLogout";
function DetailProfile({ anchorEl, setAnchorEl }) {
const [modal, setModal] = useState(false);
const handleClose = () => setAnchorEl(null);
const closeModal = () => setModal(false);
const openModal = () => setModal(true);
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<>
<ModalLogout open={modal} handleClose={closeModal} />
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<div className="flex pt-[11px] pb-[14px] flex-col items-start gap-[8px] min-w-[185px]">
<Link href="/dashboard" className="w-full">
<Button
fullWidth
variant="text"
className="!text-[#7E7E7E] !text-[14px] !font-medium !flex !justify-start !p-[8px] !gap-[5px]"
>
<ProfilePA />
پروفایل من
</Button>
</Link>
<Link
href={{
pathname: "/dashboard",
query: {
sidebar: 2,
},
}}
className="w-full"
>
<Button
fullWidth
variant="text"
className="!text-[#7E7E7E] !text-[14px] !font-medium !flex !justify-start !p-[8px] !gap-[5px]"
>
<CardDA />
تراکنش های من
</Button>
</Link>
<Button
fullWidth
color="error"
variant="contained"
onClick={openModal}
sx={{
"&.MuiButtonBase-root": {
paddingTop: "8px !important",
paddingBottom: "8px !important",
},
}}
className="!flex !w-[calc(100%-16px)] !mx-auto !text-[#FAFAFA] !text-[14px] !font-medium !items-center !justify-center !gap-[5px]"
>
<LogoutP />
خروج
</Button>
</div>
</Popover>
</>
);
}
export default DetailProfile;
+50 -26
View File
@@ -1,5 +1,5 @@
import Image from "next/image";
import { 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>
+18
View File
@@ -0,0 +1,18 @@
import { CircularProgress } from "@mui/material";
import React from "react";
function LoadingComponent({ children, loading }) {
return (
<div className="relative">
{loading ? (
<div className="absolute min-h-[80px] mt-[160px] w-full h-full flex items-center justify-center">
<CircularProgress />
</div>
) : (
children
)}
</div>
);
}
export default LoadingComponent;
+2 -2
View File
@@ -2,7 +2,7 @@ import Image from "next/image";
import Link from "next/link";
import 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>
+2 -2
View File
@@ -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
+9 -3
View File
@@ -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",
+2 -2
View File
@@ -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>
);
+7 -4
View File
@@ -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>
+5 -4
View File
@@ -7,7 +7,7 @@ function a11yProps(index) {
};
}
function Tabs({ value, handleChange, listTab, isSm, style }) {
function Tabs({ value, handleChange, listTab, inactives, isSm, style }) {
return (
<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={`
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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);
+27
View File
@@ -0,0 +1,27 @@
import ArrowUpComment from "@/components/icons/ArrowUpComment";
import { Button } from "@mui/material";
function AnswerField({ data, update, setUpdate }) {
return (
<div className="flex items-center justify-start gap-1 my-2">
<Button
className="!text-[#0FA1B3] !text-[12px] !font-medium"
onClick={() => {
data.is_open_reply = !data.is_open_reply;
setUpdate(!update);
}}
>
مشاهده پاسخ ها ({data.replies.length})
<div
className={`!mr-1 ${
data.is_open_reply ? "!rotate-0" : "!rotate-180"
}`}
>
<ArrowUpComment />
</div>
</Button>
</div>
);
}
export default AnswerField;
+2 -1
View File
@@ -1,12 +1,13 @@
import { TextField } from "@mui/material";
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
View File
@@ -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>
);
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import ItemUser from "./ItemUser";
function Replies({ data, update, setUpdate }) {
return (
<ul
className={`flex pr-12 flex-col justify-center transition-all duration-500 overflow-hidden items-start gap-12 w-full
${data.is_open_reply ? "max-h-fit mt-12" : "max-h-0"}`}
>
{data.replies.map((item, idx) => (
<ItemUser
key={idx}
data={item}
isReply={true}
update={update}
setUpdate={setUpdate}
/>
))}
</ul>
);
}
export default Replies;
+65
View File
@@ -0,0 +1,65 @@
import { Button } from "@mui/material";
import Field from "./Field";
import { useState } from "react";
import { request } from "@/services/response";
import { alert } from "@/helper";
function SendAnswer({ data }) {
const [comment, setComment] = useState("");
const [loading, setLoading] = useState(false);
const resetData = () => {
setComment("");
setLoading(false);
data.is_open_answer = false;
};
const sendReq = () => {
setLoading(true);
const body = {
comment,
doctor_id: data?.doctor?.id,
parent: data?.parent,
};
request
.postDoctorComment(body)
.then(() => {
alert(
"success",
"کامنت شما با موفقیت ثبت شد و پس از تأیید نمایش داده می‌شود."
);
resetData();
})
.catch(() => {
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
resetData();
});
};
return (
<div
className={`flex flex-col mt-2 justify-center overflow-hidden transition duration-500 items-start gap-2 mb-2
${data.is_open_answer ? "max-h-fit" : "max-h-0"}`}
>
<Field
type="text"
value={comment}
title="پاسخ شما..."
isPlaceHolder={true}
updateValue={(val) => setComment(val)}
/>
<Button
loading={loading}
onClick={sendReq}
variant="contained"
className="!p-2 !h-10 !text-[12px] !font-medium sm:!py-[10px] md:!px-[12px] !rounded-[8px]
!shadow-none sm:!shadow-[0px_4px_30px_0px_rgba(56,_160,_162,_0.30)]"
>
ارسال پاسخ
</Button>
</div>
);
}
export default SendAnswer;
+5 -8
View File
@@ -2,8 +2,8 @@
import { useState } from "react";
import 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",
};
+61 -66
View File
@@ -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>
);
}
-32
View File
@@ -1,32 +0,0 @@
import { Button } from "@mui/material";
function Hours({ doctor, hour, setHour, value, nameHours }) {
return (
<ul className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-x-[16px] gap-y-[12px] sm:gap-y-[16px] md:gap-y-[20px] lg:gap-y-[24px] mt-[24px] mb-[40px]">
{doctor[nameHours[value]].map((item, idx) => (
<li key={idx}>
<Button
onClick={() => setHour(idx)}
disabled={!item.active}
className={`
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
${idx === hour ? "!bg-[#F79463] " : ""}
`}
>
<p
className={`
text-[16px] font-semibold text-[#525252]
${item.active ? "text-[#525252]" : "text-[#D7D7D7]"}
${idx === hour ? "!text-[#FAFAFA] " : ""}
`}
>
{item.hour}
</p>
</Button>
</li>
))}
</ul>
);
}
export default Hours;
+48
View File
@@ -0,0 +1,48 @@
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import { request } from "@/services/response";
import { Button } from "@mui/material";
import Cookies from "js-cookie";
import { useParams } from "next/navigation";
import { useState } from "react";
function SendAppo({ hour, setStep }) {
const params = useParams();
const doctorId = params.doctorId;
const [loading, setLoading] = useState(false);
const sendReq = () => {
const isLogged = Cookies.get("access_token");
if (isLogged) {
setStep((step) => step + 1);
}
// setLoading(true);
// request
// .postAppointment({
// doctor_id: doctorId,
// slot: hour,
// })
// .then(() => {
// setLoading(false);
// })
// .catch(() => {
// //
// });
};
return (
<div className="w-full flex justify-end">
<Button
className="!gap-[4px] !text-[16px] !font-medium !shadow-none !px-[12px] !py-[8px] md:!py-[9px] !w-full md:!w-fit !min-w-[157px]"
variant="contained"
loading={loading}
onClick={sendReq}
disabled={!hour}
>
تایید نوبت
<ArrowLeftB />
</Button>
</div>
);
}
export default SendAppo;
+45
View File
@@ -0,0 +1,45 @@
import { Button } from "@mui/material";
import React from "react";
function List({ appo, hour, setHour, value }) {
const list = appo && appo[value ? "evening" : "morning"];
const checkHour = (item) => JSON.stringify(item) === JSON.stringify(hour);
if (list && list.length) {
return (
<ul className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-x-[16px] gap-y-[12px] sm:gap-y-[16px] md:gap-y-[20px] lg:gap-y-[24px] mt-[24px] mb-[40px]">
{list.map((item, idx) => (
<li key={idx}>
<Button
onClick={() => setHour(item)}
disabled={!item.status === "available"}
className={`
!w-full !rounded-[8px] !bg-[#EFEFEF] !flex !justify-center !items-center !py-[10px] !px-[14px] !h-[40px] md:!h-[42px] lg:!h-[44px]
${checkHour(item) ? "!bg-[#F79463] " : ""}
`}
>
<p
className={`
text-[16px] font-semibold text-[#525252]
${item.status === "available" ? "text-[#525252]" : "text-[#D7D7D7]"}
${checkHour(item) ? "!text-[#FAFAFA] " : ""}
`}
>
{item.time}
</p>
</Button>
</li>
))}
</ul>
);
} else {
return (
<p className="text-[#525252] text-[14px] md:text-[16px] font-medium py-[56px]">
در حال حاضر، نوبتی برای ساعتهای {[value ? "بعد از ظهر" : "صبح"]} موجود
نمیباشد.
</p>
);
}
}
export default List;
@@ -0,0 +1,23 @@
import { CircularProgress } from "@mui/material";
function Loading() {
return (
<div className="w-full grid place-items-center py-[48px]">
<CircularProgress
sx={{
"& .MuiCircularProgress-circle": {
stroke: "#F17732",
// strokeLinecap: "round",
},
"& .MuiCircularProgress-svg": {
// borderRadius: "50%",
// boxShadow: "inset 0 0 0 11px #D7D7D7",
},
}}
className="!mx-auto"
/>
</div>
);
}
export default Loading;
@@ -0,0 +1,18 @@
import List from "./List";
import Loading from "./Loading";
function Hours({ appo, doctor, hour, setHour, value, nameHours }) {
if (appo === "loading") {
return <Loading />;
} else if (typeof appo === "string") {
return (
<p className="text-[#525252] text-[14px] md:text-[16px] font-medium py-[56px]">
{appo}
</p>
);
} else {
return <List appo={appo} hour={hour} value={value} setHour={setHour} />;
}
}
export default Hours;
+34 -24
View File
@@ -1,19 +1,44 @@
"use client";
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>
);
}
+33 -6
View File
@@ -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) => (
+98
View File
@@ -0,0 +1,98 @@
import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui";
function AutoSelector({
list,
value,
isError,
updateData,
label,
disabled = false,
listboxProps,
name,
}) {
return (
<Autocomplete
disablePortal
options={list}
disabled={disabled}
value={value || ""}
ListboxProps={listboxProps}
getOptionLabel={(option) => {
if (typeof option === "string") return option;
return option?.name || option?.label || "";
}}
className="!w-full !rounded-lg auto-complete-selector"
onChange={(e, newValue) => {
updateData(name, newValue?.label || newValue || "");
}}
sx={{
...styleTextSelectRight,
// Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
"& .MuiOutlinedInput-notchedOutline": {
border: isError ? "1px solid red !important" : "",
},
}}
renderOption={(props, option) => (
<Button
fullWidth
{...props}
key={option.id || props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
>
{option.name || option.label}
</Button>
)}
renderInput={(params) => (
<TextField
fullWidth
{...params}
InputProps={{
...params.InputProps,
endAdornment: (
<div className="absolute left-4">
<BottomSelect />
</div>
),
}}
placeholder={label}
sx={{
borderRadius: "8px",
}}
/>
)}
/>
);
}
export default AutoSelector;
+18 -3
View File
@@ -3,17 +3,30 @@ import { styleTextSelectRight } from "@/mui";
function Field({
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",
+6 -6
View File
@@ -3,14 +3,14 @@ function HeadPageList({ children, title, detail }) {
<div className="bg-head-menu !bg-no-repeat !bg-cover !bg-center">
<div
className="
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>
+2 -1
View File
@@ -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;
+93
View File
@@ -0,0 +1,93 @@
import { Button } from "@mui/material";
// Icon
import CloseModalD from "@/components/icons/CloseModalD";
import ArrowLeftM from "@/components/icons/ArrowLeftM";
import { useState } from "react";
import AddressSelector from "./AddressSelector";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
function Content({
states,
filter,
sendReq,
setFilter,
handleClose,
setDataInURL,
filteredCities,
}) {
const [loading, setLoading] = useState(false);
const { matchedState } = getStateInfoClient();
const handleClick = () => {
setLoading(true);
sendReq().finally(() => {
setLoading(false);
handleClose();
});
};
const updateData = (name, value) => {
const newFilter =
name === "state"
? {
...filter,
state: value,
city: null,
}
: {
...filter,
[name]: value,
};
setDataInURL(newFilter);
setFilter(newFilter);
};
return (
<div>
<div className="flex justify-end w-full">
<div className="cursor-pointer" onClick={handleClose}>
<CloseModalD />
</div>
</div>
<div className="md:p-[24px] pt-0">
<p className="text-[#525252] mt-[38px] md:mt-4 text-[16px] font-medium text-center">
شهر یا استان مورد نظر را انتخاب نمایید:
</p>
<div className="flex md:min-w-[344px] flex-col mt-[52px] md:mt-[44px] mb-14 justify-center items-center gap-10">
<AddressSelector
name="state"
list={states}
label="استان"
disabled={matchedState}
value={filter?.state}
updateData={updateData}
/>
<AddressSelector
name="city"
label="شهر"
value={filter?.city}
list={filteredCities}
updateData={updateData}
disabled={!filter?.state}
/>
</div>
<div className="w-full flex justify-end">
<Button
loading={loading}
className="!w-full md:!w-fit !flex !px-3 items-center justify-center gap-1 mr-auto"
onClick={handleClick}
variant="contained"
color="primary"
disabled={!filter?.city || !filter?.state}
>
تایید و ادامه
<ArrowLeftM />
</Button>
</div>
</div>
</div>
);
}
export default Content;
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect } from "react";
import { Modal, Box } from "@mui/material";
import { styleDefault } from "@/mui";
// Data
import states from "@/data/state.json";
import cities from "@/data/city.json";
import Content from "./Content";
function ModalSearchCity({
open,
filter,
sendReq,
setOpen,
children,
setFilter,
setDataInURL,
}) {
const stateSelected = filter?.state?.name;
const [filteredCities, setFilteredCities] = useState([]);
const handleClose = () => setOpen(false);
useEffect(() => {
if (stateSelected) {
const selectedState = states.find(
(state) => state.name === stateSelected
);
if (selectedState) {
const stateId = selectedState.id;
const filteredCities = cities.filter(
(city) => city.province_id === stateId
);
setFilteredCities(filteredCities);
}
} else {
setFilteredCities([]);
}
}, [stateSelected]);
return (
<>
{/* Button Modal */}
{children}
{/* Content Modal */}
<Modal open={open} onClose={handleClose}>
<Box sx={styleDefault} className="!w-full !h-full md:!w-fit md:!h-fit">
<Content
states={states}
filter={filter}
sendReq={sendReq}
setFilter={setFilter}
handleClose={handleClose}
setDataInURL={setDataInURL}
filteredCities={filteredCities}
/>
</Box>
</Modal>
</>
);
}
export default ModalSearchCity;
+51 -20
View File
@@ -1,39 +1,70 @@
import CloseModalD from "@/components/icons/CloseModalD";
import 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>
+3 -3
View File
@@ -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>
</>
+86
View File
@@ -0,0 +1,86 @@
import BottomSelect from "@/components/icons/BottomSelect";
import { Autocomplete, Button, TextField } from "@mui/material";
import { styleTextSelectRight } from "@/mui";
function DefaultSelect({ list, value, updateData, label, error, name }) {
return (
<Autocomplete
disablePortal
options={list}
value={value || ""}
getOptionLabel={(option) => {
if (typeof option === "string") return option;
return option?.name || option?.label || "";
}}
className="!w-full !rounded-lg auto-complete-selector"
onChange={(_, newValue) => {
updateData(name, newValue?.label || newValue || "");
}}
sx={{
...styleTextSelectRight,
// Hide Icon Number
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: error
? "1px solid red !important"
: "1px solid #D7D7D7 !important",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: error ? "red !important" : "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
renderOption={(props, option) => (
<Button
fullWidth
{...props}
key={option.id || props.id}
className="!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1] hover:dark:!bg-[#35343D] !bg-[#FFF] dark:!bg-[#1F1D2B]"
>
{option.name || option.label}
</Button>
)}
renderInput={(params) => (
<TextField
fullWidth
{...params}
InputProps={{
...params.InputProps,
endAdornment: (
<div className="absolute left-4">
<BottomSelect />
</div>
),
}}
placeholder={label}
sx={{
borderRadius: "8px",
}}
/>
)}
/>
);
}
export default DefaultSelect;
+101
View File
@@ -0,0 +1,101 @@
import MenuItem from "@mui/material/MenuItem";
import ListSubheader from "@mui/material/ListSubheader";
import Select from "@mui/material/Select";
import { styleTextSelectRight } from "@/mui";
import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
function groupSpecialtiesByParent(specialties) {
const groups = [];
const parents = specialties.filter((item) => item.parent === null);
parents.forEach((parent) => {
const children = specialties.filter((item) => item.parent === parent.id);
if (children.length > 0) {
groups.push({
parent,
children,
});
}
});
return groups;
}
export default function GroupedSelect({
list,
name,
updateData,
data,
nameKey = "name",
}) {
const groupedList = groupSpecialtiesByParent(list);
const handleChange = (event) => {
const value = event.target.value;
updateData && updateData(value, "value", name);
};
return (
<div className="w-full auto-complete-selector">
<Select
value={data}
onChange={handleChange}
IconComponent={IconMultipleSelect}
MenuProps={{
style: {
maxHeight: 300,
},
}}
className="!w-full"
sx={{
...styleTextSelectRight,
"& .MuiSelect-icon": {
left: "16px",
right: "auto",
},
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
>
{groupedList.map((group) => [
<ListSubheader
key={group.parent.id}
sx={{
backgroundColor: "#35343D",
color: "#fff",
fontSize: "14px",
}}
>
{group.parent[nameKey]}
</ListSubheader>,
...group.children.map((child) => (
<MenuItem key={child.id} value={child.id}>
{child[nameKey]}
</MenuItem>
)),
])}
</Select>
</div>
);
}
+72
View File
@@ -0,0 +1,72 @@
import OutlinedInput from "@mui/material/OutlinedInput";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import { styleTextSelectRight } from "@/mui";
import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
export default function MultipleSelect({ value, updateData, name, list }) {
const handleChange = (event) => {
const {
target: { value },
} = event;
const newVal = typeof value === "string" ? value.split(",") : value;
updateData(newVal, "value", name);
};
return (
<div className="auto-complete-selector w-full">
<Select
multiple
displayEmpty
value={value}
onChange={handleChange}
input={<OutlinedInput />}
IconComponent={IconMultipleSelect}
className=""
sx={{
...styleTextSelectRight,
width: "100% !important",
"& .MuiInputBase-input": { padding: "12.5px 14px !important" },
"& .MuiInputBase-root": {
borderRadius: 2,
padding: "0 !important",
height: {
xs: "43px !important",
sm: "43px !important",
md: "46px !important",
lg: "48px !important",
},
},
"& .MuiOutlinedInput-notchedOutline": {
border: "1px solid #D7D7D7",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before, .MuiOutlinedInput-notchedOutline":
{
borderColor: "#D7D7D7 !important",
},
"& .muirtl-1kiro5a-MuiInputBase-root-MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: "1px solid #5559CE !important",
},
}}
>
{list.map((item) => (
<MenuItem
key={item.id || item.uuid}
value={item.id}
selected={value.includes(item.id)}
className={`!flex !justify-start !p-[8px] !text-start !text-[#A1A1A1]
${
value.includes(item.id)
? "dark:!bg-[#35343D] !bg-[#F1F1F1]"
: "dark:!bg-[#1F1D2B] !bg-[#FFF]"
}
hover:dark:!bg-[#35343D] hover:!bg-[#F5F5F5]`}
>
{item.name || item.label}
</MenuItem>
))}
</Select>
</div>
);
}
+12 -5
View File
@@ -1,15 +1,20 @@
import { Button } from "@mui/material";
import { 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]"
>
ذخیره
+14
View File
@@ -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);
+1 -1
View File
@@ -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"
/>
+2 -2
View File
@@ -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
View File
@@ -1,37 +1,29 @@
"use client";
import Content from "@/components/dashboard/Content";
import { defineAbilitiesFor } from "@/lib/ability";
import { getUser } from "@/lib/auth";
import { getStateInfo } from "@/lib/getStateInfo";
import { removeToken } from "@/utils";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { useState } from "react";
export default async function Dashboard({ searchParams }) {
const { matchedCity } = getStateInfo();
const user = await getUser();
import userData from "@/data/userData.json";
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;
+30 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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" />
+2 -20
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
import Layout from "@/components/layout";
import Layout from "@/components/layout/StLayout";
import NotFoundPage from "@/components/notFound";
function NotFound() {
+2 -2
View File
@@ -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>
);
-2
View File
@@ -1,5 +1,3 @@
"use client";
import DashboardPage from "@/components/panel/dashboard";
import Information from "@/data/information.json";
+11 -56
View File
@@ -1,62 +1,17 @@
"use client";
import Content from "@/components/panel/Content";
import { defineAbilitiesFor } from "@/lib/ability";
import { getUser } from "@/lib/auth";
import { redirect } from "next/navigation";
import { useEffect, useState } from "react";
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;
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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
View File
@@ -4,7 +4,7 @@ import { createTheme } from "@mui/material/styles";
const theme = createTheme({
direction: "rtl",
typography: {
fontFamily: "iran-sans",
fontFamily: "vazir",
},
components: {
MuiButton: {
+6 -3
View File
@@ -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>
);
}
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>
);
+66
View File
@@ -0,0 +1,66 @@
import Date from "./date";
// Components
import Content from "./Content";
import Layout from "@/components/layout";
import Paying from "@/components/appointment/paying";
import Detail from "@/components/appointment/detail";
import FailedPay from "@/components/appointment/failedPay";
import SuccessPay from "@/components/appointment/successPay";
function Container({
step,
data,
doctor,
setData,
setStep,
prevData,
matchedCity,
isForAnother,
setIsForAnother,
}) {
const elements = [
<Date doctor={doctor} setStep={setStep} />,
// <LogInPage setIsSendMsg={false} setStep={setStep} />,
// <VerificationPage setIsSendMsg={false} link={false} setStep={setStep} />,
<Detail
data={data}
setStep={setStep}
setData={setData}
prevData={prevData}
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
/>,
<Paying setStep={setStep} />,
<SuccessPay setStep={setStep} />,
<FailedPay setStep={setStep} />,
];
return (
<>
{/* {step === 1 || step === 2 ? (
<RegisterPage>{elements[step]}</RegisterPage>
) : ( */}
<Layout
title="رزرو"
name="booking"
disableFooter={step > 2}
matchedCity={matchedCity}
paddingBottom="pb-[80px]"
>
<Content
disableSide={step === 5 || step === 6}
isFirst={step === 0}
setStep={setStep}
doctor={doctor}
step={step}
>
{elements[step]}
</Content>
</Layout>
{/* )} */}
</>
);
}
export default Container;
@@ -1,14 +1,7 @@
import Information from "./information";
import 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;
@@ -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>
);
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import doctors from "@/data/doctors.json";
import { request } from "@/services/response";
import Cookies from "js-cookie";
import Container from "./Container";
const defaultData = {
phone: { value: "", isEdit: false },
national_code: { value: "", isEdit: false },
name: { value: "", isEdit: false },
basic_insurance: { value: "", isEdit: false },
};
function AppointmentPage({ slug, matchedCity }) {
const doctor = doctors.find((item) => item.id === +slug);
const [step, setStep] = useState(0);
const [isForAnother, setIsForAnother] = useState(false);
const [prevData, setPrevData] = useState(defaultData);
const [data, setData] = useState(defaultData);
useEffect(() => {
const userInfo = Cookies.get("userInfo");
const parsedData = userInfo && JSON.parse(userInfo);
if (userInfo && parsedData) {
request
.getUserProfile(parsedData.uuid)
.then((res) => {
if (res) {
const newData = {
uuid: res.uuid,
phone: { value: parsedData.username || "", isEdit: false },
national_code: {
value: res.national_code || "",
isEdit: res.national_code ? false : true,
},
name: { value: `${res.name} ${res.family}`, isEdit: true },
basic_insurance: {
value: res.basic_insurance,
isEdit: true,
},
};
setData(newData);
setPrevData(newData);
} else {
const emptyData = {
uuid: res.uuid,
phone: { value: parsedData.username || "", isEdit: false },
national_code: { value: "", isEdit: true },
name: { value: "", isEdit: true },
basic_insurance: {
value: res.basic_insurance,
isEdit: true,
},
};
setData(emptyData);
setPrevData(emptyData);
}
})
.catch(() => {
//
});
}
}, []);
return (
<Container
step={step}
data={data}
doctor={doctor}
setData={setData}
setStep={setStep}
prevData={prevData}
matchedCity={matchedCity}
isForAnother={isForAnother}
setIsForAnother={setIsForAnother}
/>
);
}
export default AppointmentPage;
@@ -14,7 +14,7 @@ function Detail({ doctor, step, setStep }) {
<LocationA />
</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>
@@ -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