diff --git a/app/appwrite/auth.ts b/app/appwrite/auth.ts index 00e89cc..16b504d 100644 --- a/app/appwrite/auth.ts +++ b/app/appwrite/auth.ts @@ -2,13 +2,28 @@ import { ID, OAuthProvider, Query } from "appwrite"; import { account, database, appwriteConfig } from "~/appwrite/client"; import { redirect } from "react-router"; +/** + * Helper function to query user documents by accountID + */ +const queryUserByAccountId = async ( + accountId: string, + selectFields?: string[] +) => { + const queries = [Query.equal("accountID", accountId)]; + if (selectFields && selectFields.length > 0) { + queries.push(Query.select(selectFields)); + } + + return await database.listDocuments( + appwriteConfig.databaseID!, + appwriteConfig.userCollectionID!, + queries + ); +}; + export const getExistingUser = async (id: string) => { try { - const { documents, total } = await database.listDocuments( - appwriteConfig.databaseID!, - appwriteConfig.userCollectionID!, - [Query.equal("accountID", id)] - ); + const { documents, total } = await queryUserByAccountId(id); return total > 0 ? documents[0] : null; } catch (error) { console.error("Error fetching user:", error); @@ -86,13 +101,9 @@ export const getUser = async () => { const user = await account.get(); if (!user) return redirect("/sign-in"); - const { documents } = await database.listDocuments( - appwriteConfig.databaseID!, - appwriteConfig.userCollectionID!, - [ - Query.equal("accountID", user.$id), - Query.select(["name", "email", "imageUrl", "joinedAt", "accountID"]), - ] + const { documents } = await queryUserByAccountId( + user.$id, + ["name", "email", "imageUrl", "joinedAt", "accountID"] ); return documents.length > 0 ? documents[0] : redirect("/sign-in"); diff --git a/app/hooks/useSyncfusionComponent.ts b/app/hooks/useSyncfusionComponent.ts new file mode 100644 index 0000000..fc51cd1 --- /dev/null +++ b/app/hooks/useSyncfusionComponent.ts @@ -0,0 +1,48 @@ +import { useState, useEffect, useRef } from "react"; + +/** + * Custom hook to dynamically import Syncfusion components + * @param importFn - Function that returns a promise with the Syncfusion package + * @param componentNames - Array of component names to extract from the package + * @returns Object with loaded components or null if not yet loaded + * + * @example + * const components = useSyncfusionComponent( + * () => import("@syncfusion/ej2-react-buttons"), + * ["ButtonComponent"] + * ); + * if (!components) return null; + * const { ButtonComponent } = components; + */ +export function useSyncfusionComponent>( + importFn: () => Promise, + componentNames: (keyof T)[] +): Record | null { + const [components, setComponents] = useState | null>(null); + const componentNamesRef = useRef(componentNames); + + useEffect(() => { + let isMounted = true; + + importFn() + .then((pkg) => { + if (isMounted) { + const loadedComponents: Record = {}; + componentNamesRef.current.forEach((name) => { + loadedComponents[name as string] = pkg[name]; + }); + setComponents(loadedComponents); + } + }) + .catch((error) => { + console.error("Failed to load Syncfusion component:", error); + }); + + return () => { + isMounted = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return components; +} diff --git a/app/lib/syncfusion.ts b/app/lib/syncfusion.ts new file mode 100644 index 0000000..01f99dd --- /dev/null +++ b/app/lib/syncfusion.ts @@ -0,0 +1,14 @@ +import { registerLicense } from "@syncfusion/ej2-base"; + +/** + * Initializes Syncfusion license + * Should be called once at application startup + */ +let isLicenseRegistered = false; + +export function initializeSyncfusionLicense(): void { + if (!isLicenseRegistered) { + registerLicense(import.meta.env.VITE_SYNCFUSION_LICENSE_KEY); + isLicenseRegistered = true; + } +} diff --git a/app/routes/admin/admin-layout.tsx b/app/routes/admin/admin-layout.tsx index 08f1c6a..4643f87 100644 --- a/app/routes/admin/admin-layout.tsx +++ b/app/routes/admin/admin-layout.tsx @@ -1,6 +1,6 @@ import { Outlet, redirect } from "react-router"; import { MobileSideBar, NavItems } from "../../../components/index"; -import { useEffect, useState } from "react"; +import { useSyncfusionComponent } from "~/hooks/useSyncfusionComponent"; import { account } from "~/appwrite/client"; import { getExistingUser, storeUserData } from "~/appwrite/auth"; @@ -51,15 +51,13 @@ export async function clientLoader() { } } const AdminLayout = () => { - const [SidebarComponent, setSidebarComponent] = useState(null); - - useEffect(() => { - import("@syncfusion/ej2-react-navigations").then((pkg) => { - setSidebarComponent(() => pkg.SidebarComponent); - }); - }, []); + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-navigations"), + ["SidebarComponent"] + ); - if (!SidebarComponent) return null; + if (!components) return null; + const { SidebarComponent } = components; return (
diff --git a/app/routes/admin/all-users.tsx b/app/routes/admin/all-users.tsx index f21665f..ffaf57a 100644 --- a/app/routes/admin/all-users.tsx +++ b/app/routes/admin/all-users.tsx @@ -1,7 +1,7 @@ -import { useState, useEffect } from "react"; import { Header } from "../../../components"; import { cn, formatDate } from "~/lib/utils"; import { getAllUsers } from "~/appwrite/auth"; +import { useSyncfusionComponent } from "~/hooks/useSyncfusionComponent"; import type { Route } from "./+types/all-users"; @@ -12,22 +12,14 @@ export const loader = async () => { } const Allusers = ({loaderData}: Route.ComponentProps) => { - const [GridComponent, setGridComponent] = useState(null) - const [ColumnsDirective, setColumnsDirective] = useState(null) - const [ColumnDirective, setColumnDirective] = useState(null) - const { users } = loaderData; + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-grids"), + ["GridComponent", "ColumnsDirective", "ColumnDirective"] + ); - - useEffect(() => { - import("@syncfusion/ej2-react-grids").then((pkg) => { - setGridComponent(() => pkg.GridComponent) - setColumnsDirective(() => pkg.ColumnsDirective) - setColumnDirective(() => pkg.ColumnDirective) - }) - }, []) - - if (!GridComponent || !ColumnsDirective) return null; + if (!components) return null; + const { GridComponent, ColumnsDirective, ColumnDirective } = components; return (
{ } const CreateTrip = ({ loaderData }: Route.ComponentProps) => { - const [ComboBoxComponent, setComboBoxComponent] = useState(null); - const handleSubmit = async () => { }; const handleChange = (key: keyof TripFormData, value: string | number) => { } @@ -30,13 +28,13 @@ const CreateTrip = ({ loaderData }: Route.ComponentProps) => { value: country.value, })); - useEffect(() => { - import("@syncfusion/ej2-react-dropdowns").then((pkg) => { - setComboBoxComponent(() => pkg.ComboBoxComponent) - }) - }, []); + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-dropdowns"), + ["ComboBoxComponent"] + ); - if (!ComboBoxComponent) return null + if (!components) return null; + const { ComboBoxComponent } = components; return (
diff --git a/app/routes/main.tsx b/app/routes/main.tsx index 3430d19..532e076 100644 --- a/app/routes/main.tsx +++ b/app/routes/main.tsx @@ -1,7 +1,7 @@ import React from "react"; +import { initializeSyncfusionLicense } from "~/lib/syncfusion"; -import { registerLicense } from "@syncfusion/ej2-base"; -registerLicense(import.meta.env.VITE_SYNCFUSION_LICENSE_KEY); +initializeSyncfusionLicense(); const MainPage = () => { return ( diff --git a/components/Header.tsx b/components/Header.tsx index a505a93..e560e57 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; import { Link, useLocation } from "react-router"; -import { cn } from "~/lib/utils"; +import { cn } from "~/lib/utils"; +import { useSyncfusionComponent } from "~/hooks/useSyncfusionComponent"; type Props = { title: string; @@ -19,16 +19,14 @@ type Props = { * @returns {JSX.Element} The rendered Header component. */ const Header = ({ title, descprition, ctaText, ctaUrl }: Props) => { - const [ButtonComponent, setButtonComponent] = useState(null) const location = useLocation(); + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-buttons"), + ["ButtonComponent"] + ); - useEffect(() => { - import("@syncfusion/ej2-react-buttons").then((pkg) => ( - setButtonComponent(() => pkg.ButtonComponent) - )) - }); - - if(!ButtonComponent) return null; + if (!components) return null; + const { ButtonComponent } = components; return (
diff --git a/components/MobileSideBar.tsx b/components/MobileSideBar.tsx index 83dcbea..e2c4ce6 100644 --- a/components/MobileSideBar.tsx +++ b/components/MobileSideBar.tsx @@ -1,26 +1,24 @@ -import { useRef, useState, useEffect } from "react"; +import { useRef } from "react"; import { Link } from "react-router"; import NavItems from "./NavItems"; +import { useSyncfusionComponent } from "~/hooks/useSyncfusionComponent"; +import { initializeSyncfusionLicense } from "~/lib/syncfusion"; -import { registerLicense } from "@syncfusion/ej2-base"; - -registerLicense(import.meta.env.VITE_SYNCFUSION_LICENSE_KEY); +initializeSyncfusionLicense(); const MobileSideBar = () => { - const [SidebarComponent, setSidebarComponent] = useState(null); const sideBarRef = useRef(null); + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-navigations"), + ["SidebarComponent"] + ); const toggleSidebar = () => { sideBarRef.current?.toggle(); }; - useEffect(() => { - import("@syncfusion/ej2-react-navigations").then((pkg) => { - setSidebarComponent(() => pkg.SidebarComponent); - }); - }, []); - - if (!SidebarComponent) return null; + if (!components) return null; + const { SidebarComponent } = components; return (
diff --git a/components/NavItems.tsx b/components/NavItems.tsx index c6b1a92..e24279e 100644 --- a/components/NavItems.tsx +++ b/components/NavItems.tsx @@ -1,10 +1,10 @@ import { Link, NavLink, useLoaderData, useNavigate } from "react-router"; import { sidebarItems } from "~/constants"; import { cn } from "~/lib/utils"; -import { registerLicense } from "@syncfusion/ej2-base"; +import { initializeSyncfusionLicense } from "~/lib/syncfusion"; import { logoutUser } from "~/appwrite/auth"; -registerLicense(import.meta.env.VITE_SYNCFUSION_LICENSE_KEY); +initializeSyncfusionLicense(); const NavItems = ({ handleClick }: { handleClick?: () => void }) => { const user = useLoaderData(); diff --git a/components/TripCard.tsx b/components/TripCard.tsx index 368810d..6159fe0 100644 --- a/components/TripCard.tsx +++ b/components/TripCard.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; import { Link, useLocation } from "react-router"; import { getFirstWord, cn } from "~/lib/utils"; +import { useSyncfusionComponent } from "~/hooks/useSyncfusionComponent"; const TripCard = ({ id, @@ -10,20 +10,14 @@ const TripCard = ({ tags, price, }: TripCardProps) => { - const [ChipListComponent, setChipListComponent] = useState(null); - const [ChipsDirective, setChipsDirective] = useState(null); - const [ChipDirective, setChipDirective] = useState(null); const path = useLocation(); + const components = useSyncfusionComponent( + () => import("@syncfusion/ej2-react-buttons"), + ["ChipListComponent", "ChipsDirective", "ChipDirective"] + ); - useEffect(() => { - import("@syncfusion/ej2-react-buttons").then((pkg) => { - setChipListComponent(() => pkg.ChipListComponent); - setChipsDirective(() => pkg.ChipsDirective); - setChipDirective(() => pkg.ChipDirective); - }); - }, []); - - if (!ChipListComponent || !ChipsDirective || !ChipDirective) return null; + if (!components) return null; + const { ChipListComponent, ChipsDirective, ChipDirective } = components; return (