diff --git a/.gitignore b/.gitignore index 5ef6a52..10853e6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Swagger Files +.yaml +.yml +swagger-output.json + diff --git a/README.md b/README.md index 13cbc49..741e11f 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -GP Frontend \ No newline at end of file +# GP Frontend diff --git a/app/(pages)/(admins)/admin-dashboard/clinics/page.tsx b/app/(pages)/(admins)/admin-dashboard/clinics/page.tsx new file mode 100644 index 0000000..def1c4d --- /dev/null +++ b/app/(pages)/(admins)/admin-dashboard/clinics/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { ClinicListContainer } from "@/features/admins/admin"; +import { Shield } from "lucide-react"; +import { navigationItems } from "@/constants/AdminNavigationItems"; + +export default function ClinicsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/admin-dashboard/doctors/page.tsx b/app/(pages)/(admins)/admin-dashboard/doctors/page.tsx new file mode 100644 index 0000000..abf0dd1 --- /dev/null +++ b/app/(pages)/(admins)/admin-dashboard/doctors/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/AdminNavigationItems"; +import { DoctorListContainer } from "@/features/admins/admin/components/doctors/DoctorListContainer"; + +export default function AdminDashboardPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/admin-dashboard/nurses/page.tsx b/app/(pages)/(admins)/admin-dashboard/nurses/page.tsx new file mode 100644 index 0000000..2cd2de8 --- /dev/null +++ b/app/(pages)/(admins)/admin-dashboard/nurses/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/AdminNavigationItems"; +import { NurseListContainer } from "@/features/admins/admin/components/nurses/NurseListContainer"; + +export default function AdminNursesPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/admin-dashboard/page.tsx b/app/(pages)/(admins)/admin-dashboard/page.tsx new file mode 100644 index 0000000..04ba462 --- /dev/null +++ b/app/(pages)/(admins)/admin-dashboard/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/AdminNavigationItems"; +import { DashboardContainer } from "@/features/admins/admin/components/dashboard/DashboardContainer"; + +export default function AdminDashboardPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/superadmin-dashboard/admins/page.tsx b/app/(pages)/(admins)/superadmin-dashboard/admins/page.tsx new file mode 100644 index 0000000..71446ee --- /dev/null +++ b/app/(pages)/(admins)/superadmin-dashboard/admins/page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { AdminListContainer } from "@/features/admins/super-admin"; +import { Users} from "lucide-react"; +import { navigationItems } from "@/constants/superAdminNavigationItems"; + +export default function AdminsPage() { + + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/superadmin-dashboard/clinics/page.tsx b/app/(pages)/(admins)/superadmin-dashboard/clinics/page.tsx new file mode 100644 index 0000000..9c63f1a --- /dev/null +++ b/app/(pages)/(admins)/superadmin-dashboard/clinics/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { ClinicListContainer } from "@/features/admins/super-admin"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/superAdminNavigationItems"; + +export default function ClinicsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/superadmin-dashboard/doctors/page.tsx b/app/(pages)/(admins)/superadmin-dashboard/doctors/page.tsx new file mode 100644 index 0000000..545b7f6 --- /dev/null +++ b/app/(pages)/(admins)/superadmin-dashboard/doctors/page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { DoctorListContainer } from "@/features/admins/super-admin"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/superAdminNavigationItems"; + +export default function DoctorsPage() { + + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/superadmin-dashboard/nurses/page.tsx b/app/(pages)/(admins)/superadmin-dashboard/nurses/page.tsx new file mode 100644 index 0000000..5f2a0e4 --- /dev/null +++ b/app/(pages)/(admins)/superadmin-dashboard/nurses/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/superAdminNavigationItems"; +import { NurseListContainer } from "@/features/admins/super-admin/components/nurse/NurseListContainer"; + +export default function SuperAdminNursesPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(admins)/superadmin-dashboard/page.tsx b/app/(pages)/(admins)/superadmin-dashboard/page.tsx new file mode 100644 index 0000000..a0b90ec --- /dev/null +++ b/app/(pages)/(admins)/superadmin-dashboard/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/layout/Sidebar"; +import { Users } from "lucide-react"; +import { navigationItems } from "@/constants/superAdminNavigationItems"; +import { DashboardContainer } from "@/features/admins/super-admin/components/dashboard/DashboardContainer"; + +export default function SuperAdminDashboardPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(auth)/complete-profile/page.tsx b/app/(pages)/(auth)/complete-profile/page.tsx new file mode 100644 index 0000000..587a1b5 --- /dev/null +++ b/app/(pages)/(auth)/complete-profile/page.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { CompleteProfileContainer } from "@/features/auth/complete-profile/components/CompleteProfileContainer"; +import { CompleteProfilePresentational } from "@/features/auth/complete-profile/components/CompleteProfilePresentational"; +import { useEffect, useState } from "react"; +import { useAuthFlowStore } from "@/stores/useAuthFlowStore"; +import { useAuthSync } from "@/hooks/useAuthSync"; +import { getCurrentUser } from "@/features/dashboards/dashboard/api/dashboard.api"; + +export default function CompleteProfilePage() { + const [isLoading, setIsLoading] = useState(true); + const signupMethod = useAuthFlowStore((state) => state.signupMethod); + const { user, updateAuthState } = useAuthSync(); + + useEffect(() => { + const initializeUser = async () => { + const cookies = document.cookie; + const hasAuthCookie = + cookies.includes("Authorization=") || cookies.includes("RefreshToken="); + const hasUserStateCookie = cookies.includes("UserState="); + + if (hasAuthCookie && !hasUserStateCookie) { + try { + const userData = await getCurrentUser(); + updateAuthState(userData); + + setTimeout(() => { + window.location.reload(); + }, 100); + return; + } catch (error) { + console.error("Failed:", error); + } + } + + if (!user || !user.email) { + try { + const userData = await getCurrentUser(); + updateAuthState(userData); + + setTimeout(() => { + const cookies = document.cookie.split(";"); + const userStateCookie = cookies.find((c) => + c.trim().startsWith("UserState="), + ); + console.log("šŸŖ UserState cookie:", userStateCookie); + }, 50); + } catch (error) { + console.error("Failed:", error); + } + } + setIsLoading(false); + }; + + initializeUser(); + }, [user, updateAuthState]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + const requirePhone = signupMethod === "google" || !user?.phone; + + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/(auth)/forgot-password/page.tsx b/app/(pages)/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..6fe8758 --- /dev/null +++ b/app/(pages)/(auth)/forgot-password/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + ForgotPasswordContainer, + ForgotPasswordPresentational, +} from "@/features/auth/password"; + +export default function ForgotPasswordPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/(auth)/google-callback/page.tsx b/app/(pages)/(auth)/google-callback/page.tsx new file mode 100644 index 0000000..46a04da --- /dev/null +++ b/app/(pages)/(auth)/google-callback/page.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { getCurrentUser } from "@/features/dashboards/dashboard/api/dashboard.api"; +import { useAuthSync } from "@/hooks/useAuthSync"; + +export default function GoogleCallbackPage() { + const router = useRouter(); + const { updateAuthState } = useAuthSync(); + + useEffect(() => { + const handleGoogleCallback = async () => { + try { + const userData = await getCurrentUser(); + updateAuthState(userData); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + window.location.href = "/complete-profile"; + } catch (error) { + console.error("Failed:", error); + router.push("/login"); + } + }; + + handleGoogleCallback(); + }, [router, updateAuthState]); + + return ( +
+
+
+

Completing Google Sign In...

+

Please wait...

+
+
+ ); +} diff --git a/app/(pages)/(auth)/login/page.tsx b/app/(pages)/(auth)/login/page.tsx new file mode 100644 index 0000000..8430d17 --- /dev/null +++ b/app/(pages)/(auth)/login/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +import { LoginContainer } from "@/features/auth/login/components/LoginContainer"; +import { LoginPresentational } from "@/features/auth/login/components/LoginPresentational"; +import { useUserStore } from "@/stores/useUserStore"; + +export default function LoginPage() { + + const clearUser = useUserStore((state) => state.clearUser); + + useEffect(() => { + const userStorage = localStorage.getItem('user-storage'); + if (userStorage) { + try { + const parsed = JSON.parse(userStorage); + if (parsed?.state?.user) { + localStorage.removeItem('user-storage'); + document.cookie = "UserState=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + clearUser(); + } + } catch (error) { + console.error("Error parsing user storage:", error); + } + } + }, [clearUser]); + + return ( + <> + + {(props) => } + + + ); +} diff --git a/app/(pages)/(auth)/register/page.tsx b/app/(pages)/(auth)/register/page.tsx new file mode 100644 index 0000000..410c0ff --- /dev/null +++ b/app/(pages)/(auth)/register/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { RegisterContainer } from "@/features/auth/register/components/RegisterContainer"; +import { RegisterPresentational } from "@/features/auth/register/components/RegisterPresentational"; + +export default function RegisterPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/(auth)/reset-password/page.tsx b/app/(pages)/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..ea28cd2 --- /dev/null +++ b/app/(pages)/(auth)/reset-password/page.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { + ResetPasswordContainer, + ResetPasswordPresentational, +} from "@/features/auth/password"; +import { useTranslations } from "next-intl"; +import { toast } from "@/hooks/useToast"; + +export default function ResetPasswordPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const t = useTranslations(""); + const token = searchParams.get("token"); + + useEffect(() => { + if (!token) { + toast({ + variant: "destructive", + title: t("error"), + description: t("invalidResetToken"), + }); + router.push("/login"); + } + }, [token, router, t]); + + if (!token) { + return null; + } + + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/(auth)/verify-email/page.tsx b/app/(pages)/(auth)/verify-email/page.tsx new file mode 100644 index 0000000..5d87bd6 --- /dev/null +++ b/app/(pages)/(auth)/verify-email/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { VerifyEmailContainer } from "@/features/auth/verify-email/components/VerifyEmailContainer"; +import { VerifyEmailPresentational } from "@/features/auth/verify-email/components/VerifyEmailPresentational"; + +export default function VerifyEmailPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/lobby/page.tsx b/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/lobby/page.tsx new file mode 100644 index 0000000..abab63d --- /dev/null +++ b/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/lobby/page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useParams, useSearchParams } from "next/navigation"; +import { PatientDashboardLayout } from "@/features/dashboards/dashboard"; +import { + OnlineLobbyContainer, + OnlineLobbyPresentational, +} from "@/features/appointment-session"; + +export default function PatientOnlineLobbyPage() { + const params = useParams<{ appointmentId: string }>(); + const searchParams = useSearchParams(); + const startAt = searchParams.get("startAt"); + const endAt = searchParams.get("endAt"); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/room/page.tsx b/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/room/page.tsx new file mode 100644 index 0000000..2edae37 --- /dev/null +++ b/app/(pages)/(dashboards)/dashboard/appointments/[appointmentId]/online/room/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useParams, useSearchParams } from "next/navigation"; +import { PatientDashboardLayout } from "@/features/dashboards/dashboard"; +import { + OnlineRoomContainer, + OnlineRoomPresentational, +} from "@/features/appointment-session"; + +const parseBoolean = (value: string | null, fallback: boolean) => { + if (value === null) return fallback; + return value === "true"; +}; + +export default function PatientOnlineRoomPage() { + const params = useParams<{ appointmentId: string }>(); + const searchParams = useSearchParams(); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/dashboard/appointments/page.tsx b/app/(pages)/(dashboards)/dashboard/appointments/page.tsx new file mode 100644 index 0000000..042902d --- /dev/null +++ b/app/(pages)/(dashboards)/dashboard/appointments/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { + AppointmentsContainer, + AppointmentsPresentational, + PatientDashboardLayout, +} from "@/features/dashboards/dashboard"; + +export default function AppointmentsPage() { + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/dashboard/medical-history/page.tsx b/app/(pages)/(dashboards)/dashboard/medical-history/page.tsx new file mode 100644 index 0000000..6250d1b --- /dev/null +++ b/app/(pages)/(dashboards)/dashboard/medical-history/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { + MedicalHistoryContainer, + MedicalHistoryPresentational, + PatientDashboardLayout, +} from "@/features/dashboards/dashboard"; + +export default function MedicalHistoryPage() { + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/dashboard/page.tsx b/app/(pages)/(dashboards)/dashboard/page.tsx new file mode 100644 index 0000000..5c40588 --- /dev/null +++ b/app/(pages)/(dashboards)/dashboard/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { + DashboardContainer, + DashboardPresentational, + PatientDashboardLayout, +} from "@/features/dashboards/dashboard"; + +export default function DashboardPage() { + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/announcements/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/announcements/page.tsx new file mode 100644 index 0000000..32dfae8 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/announcements/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + DoctorDashboardContainer, + DoctorAnnouncementsTab, +} from "@/features/dashboards/doctor-dashboard"; + +export default function AnnouncementsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/offline-session/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/offline-session/page.tsx new file mode 100644 index 0000000..3b2840a --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/offline-session/page.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useParams, useSearchParams } from "next/navigation"; +import { DoctorDashboardContainer } from "@/features/dashboards/doctor-dashboard"; +import { + OfflineSessionContainer, + OfflineSessionPresentational, +} from "@/features/appointment-session"; + +export default function DoctorOfflineSessionPage() { + const params = useParams<{ appointmentId: string }>(); + const searchParams = useSearchParams(); + const startAt = searchParams.get("startAt"); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/lobby/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/lobby/page.tsx new file mode 100644 index 0000000..1565b09 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/lobby/page.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useParams, useSearchParams } from "next/navigation"; +import { DoctorDashboardContainer } from "@/features/dashboards/doctor-dashboard"; +import { + OnlineLobbyContainer, + OnlineLobbyPresentational, +} from "@/features/appointment-session"; + +export default function DoctorOnlineLobbyPage() { + const params = useParams<{ appointmentId: string }>(); + const searchParams = useSearchParams(); + const startAt = searchParams.get("startAt"); + const endAt = searchParams.get("endAt"); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/room/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/room/page.tsx new file mode 100644 index 0000000..e753f08 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/online/room/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useParams, useSearchParams } from "next/navigation"; +import { DoctorDashboardContainer } from "@/features/dashboards/doctor-dashboard"; +import { + OnlineRoomContainer, + OnlineRoomPresentational, +} from "@/features/appointment-session"; + +const parseBoolean = (value: string | null, fallback: boolean) => { + if (value === null) return fallback; + return value === "true"; +}; + +export default function DoctorOnlineRoomPage() { + const params = useParams<{ appointmentId: string }>(); + const searchParams = useSearchParams(); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/soap-review/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/soap-review/page.tsx new file mode 100644 index 0000000..4e18793 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/appointments/[appointmentId]/soap-review/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { DoctorDashboardContainer } from "@/features/dashboards/doctor-dashboard"; +import { + SoapReviewContainer, + SoapReviewPresentational, +} from "@/features/appointment-session"; + +export default function DoctorSoapReviewPage() { + const params = useParams<{ appointmentId: string }>(); + + return ( + + + {(props) => } + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/appointments/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/appointments/page.tsx new file mode 100644 index 0000000..7c363dc --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/appointments/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + DoctorDashboardContainer, + AppointmentsTab, +} from "@/features/dashboards/doctor-dashboard"; + +export default function AppointmentsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/clinics/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/clinics/page.tsx new file mode 100644 index 0000000..f947cde --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/clinics/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { + DoctorDashboardContainer, + ClinicsTab, + useClinics, +} from "@/features/dashboards/doctor-dashboard"; + +export default function ClinicsPage() { + const { data: clinicsData, isLoading } = useClinics(); + + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/nurses/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/nurses/page.tsx new file mode 100644 index 0000000..40a7746 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/nurses/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + DoctorDashboardContainer, + NursesTab, +} from "@/features/dashboards/doctor-dashboard"; + +export default function NursesPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/page.tsx new file mode 100644 index 0000000..edc4b53 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + DoctorDashboardContainer, + DashboardStats, +} from "@/features/dashboards/doctor-dashboard"; + +export default function DoctorDashboardPage() { + const t = useTranslations("doctorDashboard"); + + return ( + +
+
+

{t("dashboard")}

+
+ + +
+
+ ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/schedule/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/schedule/page.tsx new file mode 100644 index 0000000..eb33fca --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/schedule/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { + DoctorDashboardContainer, + ScheduleTab, + useClinics, +} from "@/features/dashboards/doctor-dashboard"; + +export default function SchedulePage() { + const { data: clinicsData } = useClinics(); + + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/doctor-dashboard/vacation/page.tsx b/app/(pages)/(dashboards)/doctor-dashboard/vacation/page.tsx new file mode 100644 index 0000000..261b5e7 --- /dev/null +++ b/app/(pages)/(dashboards)/doctor-dashboard/vacation/page.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { + DoctorDashboardContainer, + VacationTab, + useClinics, +} from "@/features/dashboards/doctor-dashboard"; + +export default function VacationPage() { + const { data: clinicsData } = useClinics(); + const clinics = clinicsData?.data || []; + + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/nurse-dashboard/applications/page.tsx b/app/(pages)/(dashboards)/nurse-dashboard/applications/page.tsx new file mode 100644 index 0000000..1438ce9 --- /dev/null +++ b/app/(pages)/(dashboards)/nurse-dashboard/applications/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + NurseDashboardContainer, + ApplicationsTab, +} from "@/features/dashboards/nurse-dashboard"; + +export default function ApplicationsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/nurse-dashboard/appointments/page.tsx b/app/(pages)/(dashboards)/nurse-dashboard/appointments/page.tsx new file mode 100644 index 0000000..78047b4 --- /dev/null +++ b/app/(pages)/(dashboards)/nurse-dashboard/appointments/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + NurseDashboardContainer, + NurseAppointmentsTab, +} from "@/features/dashboards/nurse-dashboard"; + +export default function AppointmentsPage() { + return ( + + + + ); +} diff --git a/app/(pages)/(dashboards)/nurse-dashboard/page.tsx b/app/(pages)/(dashboards)/nurse-dashboard/page.tsx new file mode 100644 index 0000000..77a5855 --- /dev/null +++ b/app/(pages)/(dashboards)/nurse-dashboard/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { + NurseDashboardContainer, + NurseDashboardStats, +} from "@/features/dashboards/nurse-dashboard"; + +export default function NurseDashboardPage() { + const t = useTranslations("nurseDashboard"); + + return ( + +
+

+ {t("dashboard")} +

+ + +
+
+ ); +} diff --git a/app/(pages)/(dashboards)/nurse-dashboard/schedule/page.tsx b/app/(pages)/(dashboards)/nurse-dashboard/schedule/page.tsx new file mode 100644 index 0000000..6065165 --- /dev/null +++ b/app/(pages)/(dashboards)/nurse-dashboard/schedule/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { + NurseDashboardContainer, + ScheduleTab, +} from "@/features/dashboards/nurse-dashboard"; + +export default function SchedulePage() { + return ( + + + + ); +} diff --git a/app/(pages)/announcements/page.tsx b/app/(pages)/announcements/page.tsx new file mode 100644 index 0000000..b9f82fc --- /dev/null +++ b/app/(pages)/announcements/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { AnnouncementsContainer } from "@/features/join/nurse/components/announcements/AnnouncementsContainer"; +import { AnnouncementsPresentational } from "@/features/join/nurse/components/announcements/AnnouncementsPresentational"; + +export default function AnnouncementsPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/clinics/page.tsx b/app/(pages)/clinics/page.tsx new file mode 100644 index 0000000..0c6f3ed --- /dev/null +++ b/app/(pages)/clinics/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { parseAsStringEnum, useQueryState } from "nuqs"; +import { + ClinicsSidebar, + DoctorsView, + ClinicsView, + DoctorsFilters, + ClinicsFilters, +} from "@/features/clinics"; + +type View = "doctors" | "clinics"; + +export default function ClinicsPage() { + const [view] = useQueryState( + "view", + parseAsStringEnum(["doctors", "clinics"]).withDefault("doctors"), + ); + + return ( +
+
+ + +
+ {view === "doctors" ? : } +
+
+
+ ); +} diff --git a/app/(pages)/clinics/schedule/page.tsx b/app/(pages)/clinics/schedule/page.tsx new file mode 100644 index 0000000..370f64b --- /dev/null +++ b/app/(pages)/clinics/schedule/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { DoctorSchedule } from "@/features/clinics"; + +export default function SchedulePage() { + return ; +} diff --git a/app/(pages)/join/doctor/page.tsx b/app/(pages)/join/doctor/page.tsx new file mode 100644 index 0000000..1be81c2 --- /dev/null +++ b/app/(pages)/join/doctor/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { DoctorJoinContainer } from "@/features/join/doctor/components/DoctorJoinContainer"; +import { DoctorJoinPresentational } from "@/features/join/doctor/components/DoctorJoinPresentational"; + +export default function DoctorJoinPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/join/nurse/page.tsx b/app/(pages)/join/nurse/page.tsx new file mode 100644 index 0000000..106d31f --- /dev/null +++ b/app/(pages)/join/nurse/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { NurseJoinContainer } from "@/features/join/nurse/components/NurseJoinContainer"; +import { NurseJoinPresentational } from "@/features/join/nurse/components/NurseJoinPresentational"; + +export default function NurseJoinPage() { + return ( + + {(props) => } + + ); +} diff --git a/app/(pages)/settings/change-password/page.tsx b/app/(pages)/settings/change-password/page.tsx new file mode 100644 index 0000000..100aa89 --- /dev/null +++ b/app/(pages)/settings/change-password/page.tsx @@ -0,0 +1,5 @@ +import { ChangePasswordContainer } from "@/features/settings"; + +export default function ChangePasswordPage() { + return ; +} diff --git a/app/(pages)/settings/general/page.tsx b/app/(pages)/settings/general/page.tsx new file mode 100644 index 0000000..c3edea6 --- /dev/null +++ b/app/(pages)/settings/general/page.tsx @@ -0,0 +1,5 @@ +import { GeneralSettingsContainer } from "@/features/settings"; + +export default function GeneralSettingsPage() { + return ; +} diff --git a/app/(pages)/settings/layout.tsx b/app/(pages)/settings/layout.tsx new file mode 100644 index 0000000..a6dc417 --- /dev/null +++ b/app/(pages)/settings/layout.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { ReactNode } from "react"; +import { Sidebar } from "@/components/layout/Sidebar"; +import { settingsNavigationItems } from "@/constants/SettingsNavigationItems"; +import { Settings as SettingsIcon } from "lucide-react"; + +interface SettingsLayoutProps { + children: ReactNode; +} + +export default function SettingsLayout({ children }: SettingsLayoutProps) { + return ( + + {children} + + ); +} diff --git a/app/(pages)/settings/page.tsx b/app/(pages)/settings/page.tsx new file mode 100644 index 0000000..13bc5f6 --- /dev/null +++ b/app/(pages)/settings/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function SettingsPage() { + redirect("/settings/general"); +} diff --git a/app/(pages)/settings/profile/page.tsx b/app/(pages)/settings/profile/page.tsx new file mode 100644 index 0000000..c22d209 --- /dev/null +++ b/app/(pages)/settings/profile/page.tsx @@ -0,0 +1,5 @@ +import { UpdateProfileContainer } from "@/features/settings"; + +export default function ProfilePage() { + return ; +} diff --git a/app/api/auth/google-callback/route.ts b/app/api/auth/google-callback/route.ts new file mode 100644 index 0000000..bc8d8e4 --- /dev/null +++ b/app/api/auth/google-callback/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +export async function GET(request: NextRequest) { + + try { + const cookieStore = await cookies(); + + const authToken = cookieStore.get("Authorization")?.value; + const refreshToken = cookieStore.get("RefreshToken")?.value; + + if (!authToken && !refreshToken) { + console.error("No auth tokens found in callback"); + return NextResponse.redirect( + new URL("/login?error=auth_failed", request.url) + ); + } + + const apiUrl = process.env.NEXT_PUBLIC_API_URL; + const baseUrl = apiUrl?.replace(/\/api$/, "") || "http://localhost:3000"; + const userDataUrl = `${baseUrl}/auth/google/userData`; + + let userState = { + isVerified: true, + hasCompletedProfile: false, + }; + + try { + const userResponse = await fetch(userDataUrl, { + headers: { + Authorization: `Bearer ${authToken}`, + ...(refreshToken ? { Cookie: `RefreshToken=${refreshToken}` } : {}), + }, + credentials: "include", + }); + + if (userResponse.ok) { + const responseData = await userResponse.json(); + const userData = responseData.data; + + userState = { + isVerified: userData.isVerified !== false, + hasCompletedProfile: userData.hasCompletedProfile || false, + }; + + } else { + const errorText = await userResponse.text(); + console.warn("Error response:", errorText); + } + } catch (error) { + console.error("Error fetching:", error); + } + const redirectUrl = userState.hasCompletedProfile + ? "/dashboard" + : "/complete-profile"; + + const response = NextResponse.redirect(new URL(redirectUrl, request.url)); + + response.cookies.set("UserState", JSON.stringify(userState), { + httpOnly: false, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 7, + }); + + return response; + } catch (error) { + console.error("Google OAuth callback error:", error); + return NextResponse.redirect( + new URL("/login?error=callback_failed", request.url) + ); + } +} diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts new file mode 100644 index 0000000..a50970f --- /dev/null +++ b/app/api/auth/refresh/route.ts @@ -0,0 +1,198 @@ +import { NextRequest, NextResponse } from "next/server"; + +const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000"; + +export async function POST(request: NextRequest) { + try { + const authCookie = request.cookies.get("Authorization"); + const refreshCookie = request.cookies.get("RefreshToken"); + + if (!refreshCookie) { + console.error( + "āŒ [Refresh Route] No refresh token found in request cookies", + ); + const response = NextResponse.json( + { + messageEn: "No refresh token found", + messageAr: "لم ŁŠŲŖŁ… Ų§Ł„Ų¹Ų«ŁˆŲ± على رمز Ų§Ł„ŲŖŲ­ŲÆŁŠŲ«", + }, + { status: 401 }, + ); + + response.cookies.set("Authorization", "", { maxAge: 0, path: "/" }); + response.cookies.set("RefreshToken", "", { maxAge: 0, path: "/" }); + response.cookies.set("UserState", "", { maxAge: 0, path: "/" }); + response.cookies.set("AuthFailure", "true", { maxAge: 60, path: "/" }); + + return response; + } + + const cookieHeader = authCookie?.value + ? `Authorization=${authCookie.value}; RefreshToken=${refreshCookie.value}` + : `RefreshToken=${refreshCookie.value}`; + + const response = await fetch(`${API_URL}/auth/refresh`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Cookie: cookieHeader, + }, + credentials: "include", + }); + + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error( + "āŒ [Refresh Route] Failed to parse backend response", + parseError, + ); + + if (response.status === 401) { + const errorResponse = NextResponse.json( + { + messageEn: "Invalid refresh token", + messageAr: "رمز تحديث غير صالح", + }, + { status: 401 }, + ); + errorResponse.cookies.set("Authorization", "", { + maxAge: 0, + path: "/", + }); + errorResponse.cookies.set("RefreshToken", "", { maxAge: 0, path: "/" }); + errorResponse.cookies.set("UserState", "", { maxAge: 0, path: "/" }); + errorResponse.cookies.set("AuthFailure", "true", { + maxAge: 60, + path: "/", + }); + return errorResponse; + } + + return NextResponse.json( + { + messageEn: "Failed to process refresh response", + messageAr: "فؓل معالجة Ų§Ų³ŲŖŲ¬Ų§ŲØŲ© Ų§Ł„ŲŖŲ­ŲÆŁŠŲ«", + }, + { status: 500 }, + ); + } + + if (!response.ok) { + console.error( + `āŒ [Refresh Route] Backend refresh failed with status: ${response.status}`, + data, + ); + + const errorResponse = NextResponse.json(data, { + status: response.status, + }); + + if (response.status === 401) { + console.error( + "šŸ” [Refresh Route] Unauthorized - clearing all auth cookies", + ); + errorResponse.cookies.set("Authorization", "", { + maxAge: 0, + path: "/", + }); + errorResponse.cookies.set("RefreshToken", "", { maxAge: 0, path: "/" }); + errorResponse.cookies.set("UserState", "", { maxAge: 0, path: "/" }); + errorResponse.cookies.set("AuthFailure", "true", { + maxAge: 60, + path: "/", + }); + } else { + console.warn( + "āš ļø [Refresh Route] Backend error (not 401) - keeping cookies, might retry later", + ); + } + + return errorResponse; + } + + const nextResponse = NextResponse.json(data, { status: 200 }); + + const setCookieHeaders = response.headers.getSetCookie(); + if (setCookieHeaders && setCookieHeaders.length > 0) { + setCookieHeaders.forEach((cookie, index) => { + nextResponse.headers.append("Set-Cookie", cookie); + + const cookieMatch = cookie.match(/^([^=]+)=([^;]+)/); + if (cookieMatch) { + const [, name, value] = cookieMatch; + + const maxAgeMatch = cookie.match(/Max-Age=(\d+)/); + const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1]) : undefined; + + nextResponse.cookies.set(name, value, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: maxAge, + }); + } + }); + } else { + console.warn( + "āš ļø [Refresh Route] No Set-Cookie headers from backend - cookies won't be updated!", + ); + console.warn( + "āš ļø [Refresh Route] Backend might not be setting cookies in response", + ); + } + + if (data?.data?.user) { + const user = data.data.user; + const userState = { + isVerified: user.isVerified, + hasCompletedProfile: user.hasCompletedProfile, + role: user.role, + }; + + nextResponse.cookies.set("UserState", JSON.stringify(userState), { + httpOnly: false, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 7, + }); + + nextResponse.cookies.set("AuthFailure", "", { maxAge: 0, path: "/" }); + } + + return nextResponse; + } catch (error) { + console.error("āŒ [Refresh Route] Unexpected error:", error); + + const isNetworkError = + error instanceof TypeError && error.message.includes("fetch"); + + if (isNetworkError) { + console.error("🌐 [Refresh Route] Network error - backend might be down"); + return NextResponse.json( + { + messageEn: "Unable to connect to authentication server", + messageAr: "غير قادر على الاتصال ŲØŲ®Ų§ŲÆŁ… المصادقة", + }, + { status: 503 }, + ); + } + + const errorResponse = NextResponse.json( + { + messageEn: "Failed to refresh token", + messageAr: "فؓل تحديث الرمز", + }, + { status: 500 }, + ); + + console.warn( + "āš ļø [Refresh Route] Unexpected error - keeping cookies for potential retry", + ); + + return errorResponse; + } +} diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..ab9bab8 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index a316cee..911b8cd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,115 +3,257 @@ @custom-variant dark (&:is(.dark *)); +:root { + --background: oklch(0.9730 0.0133 286.1503); + --foreground: oklch(0.3015 0.0572 282.4176); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.3015 0.0572 282.4176); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.3015 0.0572 282.4176); + --primary: oklch(0.5417 0.1790 288.0332); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9174 0.0435 292.6901); + --secondary-foreground: oklch(0.4143 0.1039 288.1742); + --muted: oklch(0.9580 0.0133 286.1454); + --muted-foreground: oklch(0.5426 0.0465 284.7435); + --accent: oklch(0.9221 0.0373 262.1410); + --accent-foreground: oklch(0.3015 0.0572 282.4176); + --destructive: oklch(0.6861 0.2061 14.9941); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9115 0.0216 285.9625); + --input: oklch(0.9115 0.0216 285.9625); + --ring: oklch(0.5417 0.1790 288.0332); + --chart-1: oklch(0.5417 0.1790 288.0332); + --chart-2: oklch(0.7042 0.1602 288.9880); + --chart-3: oklch(0.5679 0.2113 276.7065); + --chart-4: oklch(0.6356 0.1922 281.8054); + --chart-5: oklch(0.4509 0.1758 279.3838); + --sidebar: oklch(0.9580 0.0133 286.1454); + --sidebar-foreground: oklch(0.3015 0.0572 282.4176); + --sidebar-primary: oklch(0.5417 0.1790 288.0332); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9221 0.0373 262.1410); + --sidebar-accent-foreground: oklch(0.3015 0.0572 282.4176); + --sidebar-border: oklch(0.9115 0.0216 285.9625); + --sidebar-ring: oklch(0.5417 0.1790 288.0332); + --font-sans: Inter, sans-serif; + --font-serif: Georgia, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0px; + --shadow-y: 4px; + --shadow-blur: 10px; + --shadow-spread: 0px; + --shadow-opacity: 0.12; + --shadow-color: hsl(240 30% 25%); + --shadow-2xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-sm: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12); + --shadow: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12); + --shadow-md: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 2px 4px -1px hsl(240 30% 25% / 0.12); + --shadow-lg: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 4px 6px -1px hsl(240 30% 25% / 0.12); + --shadow-xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 8px 10px -1px hsl(240 30% 25% / 0.12); + --shadow-2xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.30); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark { + --background: oklch(0.1743 0.0227 283.7998); + --foreground: oklch(0.9185 0.0257 285.8834); + --card: oklch(0.2284 0.0384 282.9324); + --card-foreground: oklch(0.9185 0.0257 285.8834); + --popover: oklch(0.2284 0.0384 282.9324); + --popover-foreground: oklch(0.9185 0.0257 285.8834); + --primary: oklch(0.7162 0.1597 290.3962); + --primary-foreground: oklch(0.1743 0.0227 283.7998); + --secondary: oklch(0.3139 0.0736 283.4591); + --secondary-foreground: oklch(0.8367 0.0849 285.9111); + --muted: oklch(0.2710 0.0621 281.4377); + --muted-foreground: oklch(0.7166 0.0462 285.1741); + --accent: oklch(0.3354 0.0828 280.9705); + --accent-foreground: oklch(0.9185 0.0257 285.8834); + --destructive: oklch(0.6861 0.2061 14.9941); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.3261 0.0597 282.5832); + --input: oklch(0.3261 0.0597 282.5832); + --ring: oklch(0.7162 0.1597 290.3962); + --chart-1: oklch(0.7162 0.1597 290.3962); + --chart-2: oklch(0.6382 0.1047 274.9117); + --chart-3: oklch(0.7482 0.1235 244.7492); + --chart-4: oklch(0.7124 0.0977 186.6761); + --chart-5: oklch(0.7546 0.1831 346.8124); + --sidebar: oklch(0.2284 0.0384 282.9324); + --sidebar-foreground: oklch(0.9185 0.0257 285.8834); + --sidebar-primary: oklch(0.7162 0.1597 290.3962); + --sidebar-primary-foreground: oklch(0.1743 0.0227 283.7998); + --sidebar-accent: oklch(0.3354 0.0828 280.9705); + --sidebar-accent-foreground: oklch(0.9185 0.0257 285.8834); + --sidebar-border: oklch(0.3261 0.0597 282.5832); + --sidebar-ring: oklch(0.7162 0.1597 290.3962); + --font-sans: Inter, sans-serif; + --font-serif: Georgia, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0px; + --shadow-y: 4px; + --shadow-blur: 10px; + --shadow-spread: 0px; + --shadow-opacity: 0.12; + --shadow-color: hsl(240 30% 25%); + --shadow-2xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-xs: 0px 4px 10px 0px hsl(240 30% 25% / 0.06); + --shadow-sm: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12); + --shadow: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 1px 2px -1px hsl(240 30% 25% / 0.12); + --shadow-md: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 2px 4px -1px hsl(240 30% 25% / 0.12); + --shadow-lg: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 4px 6px -1px hsl(240 30% 25% / 0.12); + --shadow-xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.12), 0px 8px 10px -1px hsl(240 30% 25% / 0.12); + --shadow-2xl: 0px 4px 10px 0px hsl(240 30% 25% / 0.30); +} + @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); } -:root { - --radius: 0.65rem; - --background: oklch(1 0 0); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.488 0.243 264.376); - --primary-foreground: oklch(0.97 0.014 254.604); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.967 0.001 286.375); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.809 0.105 251.813); - --chart-2: oklch(0.623 0.214 259.815); - --chart-3: oklch(0.546 0.245 262.881); - --chart-4: oklch(0.488 0.243 264.376); - --chart-5: oklch(0.424 0.199 265.638); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.141 0.005 285.823); - --sidebar-primary: oklch(0.546 0.245 262.881); - --sidebar-primary-foreground: oklch(0.97 0.014 254.604); - --sidebar-accent: oklch(0.967 0.001 286.375); - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); - --sidebar-border: oklch(0.92 0.004 286.32); - --sidebar-ring: oklch(0.708 0 0); +input[type="time"]::-webkit-calendar-picker-indicator { + display: none; } -.dark { - --background: oklch(0.141 0.005 285.823); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.488 0.243 264.376); - --primary-foreground: oklch(0.97 0.014 254.604); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.809 0.105 251.813); - --chart-2: oklch(0.623 0.214 259.815); - --chart-3: oklch(0.546 0.245 262.881); - --chart-4: oklch(0.488 0.243 264.376); - --chart-5: oklch(0.424 0.199 265.638); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.623 0.214 259.815); - --sidebar-primary-foreground: oklch(0.97 0.014 254.604); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.439 0 0); +input[type="time"]::-webkit-inner-spin-button { + display: none; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; } +::-webkit-scrollbar-thumb { + background: oklch(0.5417 0.1790 288.0332 / 0.25); + border-radius: 100px; + border: 2px solid transparent; + background-clip: padding-box; + transition: background 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: oklch(0.5417 0.1790 288.0332 / 0.55); + background-clip: padding-box; +} + +::-webkit-scrollbar-thumb:active { + background: oklch(0.5417 0.1790 288.0332 / 0.80); + background-clip: padding-box; +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +.dark ::-webkit-scrollbar-thumb { + background: oklch(0.7162 0.1597 290.3962 / 0.25); + background-clip: padding-box; +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: oklch(0.7162 0.1597 290.3962 / 0.55); + background-clip: padding-box; +} + +.dark ::-webkit-scrollbar-thumb:active { + background: oklch(0.7162 0.1597 290.3962 / 0.80); + background-clip: padding-box; +} + +* { + scrollbar-width: thin; + scrollbar-color: oklch(0.5417 0.1790 288.0332 / 0.30) transparent; +} + +.dark * { + scrollbar-color: oklch(0.7162 0.1597 290.3962 / 0.30) transparent; +} + +.scrollbar-hidden { + scrollbar-width: none; + -ms-overflow-style: none; +} +.scrollbar-hidden::-webkit-scrollbar { + display: none; +} + +.scrollbar-thick::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.scrollbar-overlay { + overflow: overlay; +} @layer base { * { diff --git a/app/layout.tsx b/app/layout.tsx index 5629056..779db01 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,8 +3,15 @@ import { Geist, Geist_Mono } from "next/font/google"; import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; import { cookies } from "next/headers"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; import { ThemeProvider } from "@/contexts/ThemeProvider"; import { LanguageProvider } from "@/contexts/LanguageProvider"; +import { UiPreferencesProvider } from "@/contexts/UiPreferencesProvider"; +import { SocketProvider } from "@/contexts/SocketProvider"; +import { QueryProvider } from "@/contexts/QueryProvider"; +import { Toaster } from "@/components/ui/toaster"; +import { Navbar, Footer } from "@/components/layout"; +import { InteractiveBackgroundLayer } from "@/components/ui/InteractiveBackgroundLayer"; import "./globals.css"; const geistSans = Geist({ @@ -18,8 +25,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "EMR Blockchain System", - description: "Blockchain-based Electronic Medical Records System", + title: "HoloCura", + description: "One Platform. Complete Care.", }; export default async function RootLayout({ @@ -40,13 +47,27 @@ export default async function RootLayout({ - - - - {children} - - - + + + + + + + + +
+ +
{children}
+
+