diff --git a/client/app/(patient)/profile/@appointments/page.tsx b/client/app/(patient)/profile/@appointments/page.tsx index cadb05cd..8409588d 100644 --- a/client/app/(patient)/profile/@appointments/page.tsx +++ b/client/app/(patient)/profile/@appointments/page.tsx @@ -5,10 +5,10 @@ import { useGetAppointmentsPatient } from "@/lib/hooks/appointment/useAppointmen import { useRouter } from "next/navigation"; import { format } from "date-fns"; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; import GetStatusBadge from "@/components/doctor/appointment/GetStatusBadge"; import { Calendar, FileText, Video, User } from "lucide-react"; import { useState } from "react"; +import { ButtonV2 } from "@/components/common/ButtonV2"; const AppointmentsPageSection = ({ searchParams }: { searchParams: { page: number } }) => { const page = searchParams.page || 0; @@ -60,14 +60,14 @@ const AppointmentsPageSection = ({ searchParams }: { searchParams: { page: numbe - + ))} diff --git a/client/app/(patient)/profile/layout.tsx b/client/app/(patient)/profile/layout.tsx index 4ec67198..c11c31c3 100644 --- a/client/app/(patient)/profile/layout.tsx +++ b/client/app/(patient)/profile/layout.tsx @@ -47,8 +47,8 @@ const ProfilePageLayout = ({ children, appointments }: Props) => { } return ( -
-
+
+
{section === "profile" && children} {section === "appointments" && appointments} diff --git a/client/app/(patient)/profile/page.tsx b/client/app/(patient)/profile/page.tsx index 1755b058..48e5f63b 100644 --- a/client/app/(patient)/profile/page.tsx +++ b/client/app/(patient)/profile/page.tsx @@ -1,11 +1,11 @@ 'use client' import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import Image from "next/image"; import { useState } from "react"; import UpdateProfilePatient from "@/components/models/patient/UpdateProfilePatient"; import { useGetPatientProfile } from "@/lib/hooks/patient/usePatient"; +import { ButtonV2 } from "@/components/common/ButtonV2"; export default function PatientProfilePage() { const { data: patientData, isLoading, refetch } = useGetPatientProfile(); @@ -31,15 +31,15 @@ export default function PatientProfilePage() { Personal Information - + {infoItems.map((item, index) => ( diff --git a/client/components/common/ButtonV2.tsx b/client/components/common/ButtonV2.tsx new file mode 100644 index 00000000..080db6a3 --- /dev/null +++ b/client/components/common/ButtonV2.tsx @@ -0,0 +1,79 @@ +import * as React from "react" +import { Slot, Slottable } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-zinc-800 text-zinc-100 hover:bg-zinc-700", + destructive: "bg-red-900 text-red-100 hover:bg-red-800", + outline: "border border-zinc-700 bg-transparent hover:bg-zinc-800 hover:text-zinc-100", + secondary: "bg-zinc-700 text-zinc-100 hover:bg-zinc-600", + ghost: "hover:bg-zinc-800 hover:text-zinc-100", + link: "text-zinc-100 underline-offset-4 hover:underline", + expandIcon: "group relative text-zinc-100 bg-zinc-800 hover:bg-zinc-700", + ringHover: "bg-zinc-800 text-zinc-100 transition-all duration-300 hover:bg-zinc-700 hover:ring-2 hover:ring-zinc-600 hover:ring-offset-2 hover:ring-offset-zinc-900", + shine: "text-zinc-100 animate-shine bg-gradient-to-r from-zinc-800 via-zinc-700 to-zinc-800 bg-[length:400%_100%]", + gooeyRight: "text-zinc-100 relative bg-zinc-800 z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-zinc-700 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%]", + gooeyLeft: "text-zinc-100 relative bg-zinc-800 z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-zinc-700 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%]", + linkHover1: "relative text-zinc-100 after:absolute after:bg-zinc-100 after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300", + linkHover2: "relative text-zinc-100 after:absolute after:bg-zinc-100 after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +interface IconProps { + Icon: React.ElementType + iconPlacement: "left" | "right" +} + +interface IconRefProps { + Icon?: never + iconPlacement?: undefined +} + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +export type ButtonIconProps = IconProps | IconRefProps + +const ButtonV2 = React.forwardRef( + ({ className, variant, size, asChild = false, Icon, iconPlacement, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + {Icon && iconPlacement === "left" && ( +
+ +
+ )} + {props.children} + {Icon && iconPlacement === "right" && ( +
+ +
+ )} +
+ ) + } +) +ButtonV2.displayName = "Button" + +export { ButtonV2, buttonVariants } \ No newline at end of file diff --git a/client/components/layout/NavBar.tsx b/client/components/layout/NavBar.tsx index 363b6c11..f7bf9a7c 100644 --- a/client/components/layout/NavBar.tsx +++ b/client/components/layout/NavBar.tsx @@ -21,6 +21,7 @@ import { useLogoutMutation } from "@/lib/hooks/patient/usePatientAuth"; import { toast } from "../ui/use-toast"; import { useState } from "react"; import LogoutModel from "../models/LogoutModel"; +import { ButtonV2 } from "../common/ButtonV2"; export const NavBar = () => { const path = usePathname(); @@ -65,7 +66,12 @@ export const NavBar = () => { } }; - const handleLinkClick = () => { + const handleLinkHome = () => { + setIsSheetOpen(false); + }; + + const handleLinkClick = (link:string) => { + route.push(link) setIsSheetOpen(false); }; @@ -73,18 +79,18 @@ export const NavBar = () => {
@@ -114,7 +120,7 @@ export const NavBar = () => { @@ -143,7 +149,7 @@ export const NavBar = () => { - + {patientToken !== "" ? ( diff --git a/client/components/patient/clinicians/DoctorsSection.tsx b/client/components/patient/clinicians/DoctorsSection.tsx index 8298b994..dd63d861 100644 --- a/client/components/patient/clinicians/DoctorsSection.tsx +++ b/client/components/patient/clinicians/DoctorsSection.tsx @@ -1,7 +1,7 @@ 'use client' import React, { useCallback, useMemo, useState } from "react" -import { IDoctor, PaginatedResult } from "@/types" +import { IDoctor } from "@/types" import { DoctorCard } from "@/components/patient/clinicians/DoctorCard" import Pagination from "@/components/navigation/Pagination" @@ -11,19 +11,21 @@ interface DoctorPaginationProps { export default function DoctorPagination({ initialData }: DoctorPaginationProps) { const [currentPage, setCurrentPage] = useState(1) - const [data] = useState(initialData) + const [data] = useState(initialData) const pageSize = 2 const totalPages = useMemo(() => Math.ceil(initialData.length / pageSize), [initialData.length, pageSize]) - const paginatedItems = useMemo(() => + const paginatedItems = useMemo(() => initialData.slice((currentPage - 1) * pageSize, currentPage * pageSize), [initialData, currentPage, pageSize] ) const handlePageChange = useCallback((newPage: number) => { - setCurrentPage(newPage) - }, []) + if (newPage > 0 && newPage <= totalPages) { + setCurrentPage(newPage) + } + }, [totalPages]) if (initialData.length === 0) { return

No doctors available at the moment. Please check back later.

@@ -46,4 +48,4 @@ export default function DoctorPagination({ initialData }: DoctorPaginationProps) />
) -} \ No newline at end of file +} diff --git a/client/components/patient/home/ImageSlider.tsx b/client/components/patient/home/ImageSlider.tsx index 5bcee13a..df7723b2 100644 --- a/client/components/patient/home/ImageSlider.tsx +++ b/client/components/patient/home/ImageSlider.tsx @@ -3,8 +3,11 @@ import { motion } from "framer-motion"; import React from "react"; import { ImagesSlider } from "@/components/ui/images-slider"; import { SliderImages } from "@/constants"; +import { useAuth } from "@/lib/hooks/useAuth"; +import { ButtonV2 } from "@/components/common/ButtonV2"; const ImageSlider = () => { + const { patientToken } = useAuth() return ( { Virtual Specialty Care for Everyone.
- - +
diff --git a/client/components/patient/profile/NavSection.tsx b/client/components/patient/profile/NavSection.tsx index 11ba1537..333bd931 100644 --- a/client/components/patient/profile/NavSection.tsx +++ b/client/components/patient/profile/NavSection.tsx @@ -3,9 +3,9 @@ import { useState } from "react"; import Image from "next/image"; import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; import { IPatient } from "@/types"; import UploadProfileModel from "@/components/models/patient/UploadProfileModel"; +import { ButtonV2 } from "@/components/common/ButtonV2"; interface Props { setSection: (state: "profile" | "appointments" ) => void; @@ -44,13 +44,13 @@ export default function NavSection({ setSection, patientData, refetch }: Props) height={100} className="rounded-full border-4 border-white h-32 w-32" /> - +

{patientData.name}

@@ -60,12 +60,12 @@ export default function NavSection({ setSection, patientData, refetch }: Props)
- - +
diff --git a/client/components/skeletons/ProfilePage.tsx b/client/components/skeletons/ProfilePage.tsx index acff3d87..bd5ad2f7 100644 --- a/client/components/skeletons/ProfilePage.tsx +++ b/client/components/skeletons/ProfilePage.tsx @@ -29,45 +29,17 @@ export default function ProfileSkeleton() { {/* Profile Section Skeleton */} - - + +
{[...Array(6)].map((_, i) => (
- - + +
))}
- - {/* Appointments Section Skeleton */} - - - {[...Array(3)].map((_, i) => ( -
-
- - -
- -
- ))} -
- - {/* Medical Records Section Skeleton */} - - - {[...Array(3)].map((_, i) => ( -
-
- - -
- -
- ))} -
); diff --git a/client/components/ui/button.tsx b/client/components/ui/button.tsx index 6dffc09a..390ee952 100644 --- a/client/components/ui/button.tsx +++ b/client/components/ui/button.tsx @@ -11,7 +11,7 @@ const buttonVariants = cva( variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-red-500 text-white hover:bg-red-600", + "bg-red-900 text-red-100 hover:bg-red-800", outline: "border border-dark-500 bg-dark-400 text-white hover:bg-dark-500", secondary: "bg-dark-400 text-white hover:bg-dark-500", ghost: "hover:bg-dark-400 hover:text-white", diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 131a04fc..24f96f91 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -2,78 +2,84 @@ import type { Config } from "tailwindcss"; import { fontFamily } from "tailwindcss/defaultTheme"; const config = { - darkMode: ["class"], - content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"], - prefix: "", - theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px' - } - }, - toolTip: ' bg-green-700 bg-opacity-55 border-white cursor-pointer', - extend: { - screens: { - md: '732px', - 'md-cus': '930px', - 'admin-nav': '635px' - }, - colors: { - green: { - '500': '#24AE7C', - '600': '#0D2A1F' - }, - blue: { - '500': '#79B5EC', - '600': '#152432' - }, - red: { - '500': '#F37877', - '600': '#3E1716', - '700': '#F24E43' - }, - light: { - '200': '#E8E9E9' - }, - dark: { - '200': '#0D0F10', - '300': '#131619', - '400': '#1A1D21', - '500': '#363A3D', - '600': '#76828D', - '700': '#ABB8C4' - } - }, - fontFamily: { - sans: ["var(--font-sans)", ...fontFamily.sans] - }, - keyframes: { - 'accordion-down': { - from: { - height: '0' - }, - to: { - height: 'var(--radix-accordion-content-height)' - } - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)' - }, - to: { - height: '0' - } - } - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' - } - } - }, - plugins: [require("tailwindcss-animate"), "prettier-plugin-tailwindcss"], + darkMode: ["class"], + content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"], + prefix: "", + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + toolTip: ' bg-green-700 bg-opacity-55 border-white cursor-pointer', + extend: { + screens: { + md: '732px', + 'md-cus': '930px', + 'admin-nav': '635px' + }, + colors: { + green: { + '500': '#24AE7C', + '600': '#0D2A1F' + }, + blue: { + '500': '#79B5EC', + '600': '#152432' + }, + red: { + '500': '#F37877', + '600': '#3E1716', + '700': '#F24E43' + }, + light: { + '200': '#E8E9E9' + }, + dark: { + '200': '#0D0F10', + '300': '#131619', + '400': '#1A1D21', + '500': '#363A3D', + '600': '#76828D', + '700': '#ABB8C4' + } + }, + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans] + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + "shine": { + from: { backgroundPosition: '200% 0' }, + to: { backgroundPosition: '-200% 0' }, + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + "shine": "shine 8s ease-in-out infinite", + + }, + } + }, + plugins: [require("tailwindcss-animate"), "prettier-plugin-tailwindcss"], } satisfies Config; export default config; diff --git a/server/src/presentation/routers/index.ts b/server/src/presentation/routers/index.ts index dada1f50..adbba627 100644 --- a/server/src/presentation/routers/index.ts +++ b/server/src/presentation/routers/index.ts @@ -27,11 +27,11 @@ const unauthenticatedController = new UnauthenticatedControllers(unauthenticated const errorHandler = new ErrorHandler(); app.get('/doctors', unauthenticatedController.getDoctors.bind(unauthenticatedController)) +app.use("/doctor/auth", doctorAuthentication); app.use("/patient/auth", patientAuthentication); app.use("/patient", authorizePatient.exec, protectedRoutes); app.use("/admin/auth", adminAuthentication); app.use("/admin", authorizeAdmin.exec, protectedAdminRoutes); -app.use("/doctor/auth", doctorAuthentication); app.use('/slots', slotRoutes); app.use('/appointments', appointmentRoutes) diff --git a/server/src/presentation/routers/slots/SlotsRoutes.ts b/server/src/presentation/routers/slots/SlotsRoutes.ts index c20118e2..c47dfabe 100644 --- a/server/src/presentation/routers/slots/SlotsRoutes.ts +++ b/server/src/presentation/routers/slots/SlotsRoutes.ts @@ -19,8 +19,8 @@ const slotUseCase = new SlotUseCase(slotRepository, appointmentRepository, valid const slotController = new SlotController(slotUseCase); router.post('/day', authorizeDoctor.exec, slotController.createManyByDay.bind(slotController)); -router.post('/all-days', authorizeDoctor.exec, slotController.createForAllDays.bind(slotController)); router.delete('/day', authorizeDoctor.exec, slotController.deleteManyByDay.bind(slotController)); +router.post('/all-days', authorizeDoctor.exec, slotController.createForAllDays.bind(slotController)); router.delete('/all-days', authorizeDoctor.exec, slotController.deleteForAllDays.bind(slotController)); router.get('/doctor', authorizeDoctor.exec, slotController.getAllDoctorSlots.bind(slotController)); router.get('/:doctorId', slotController.getAllSlotsByDoctorId.bind(slotController));