diff --git a/apicalls/basics.ts b/apicalls/basics.ts index 3c52d3b..f1b03f5 100644 --- a/apicalls/basics.ts +++ b/apicalls/basics.ts @@ -39,7 +39,6 @@ class FilterList { this.orderType = this.orderType.startsWith("a") ? "asc" : "desc"; this.includeJoins = includeJoins ?? []; this.query = query ?? {}; - } filter() { return { diff --git a/apicalls/building/build.tsx b/apicalls/building/build.tsx index 9479de1..200cf9a 100644 --- a/apicalls/building/build.tsx +++ b/apicalls/building/build.tsx @@ -13,6 +13,7 @@ const buildUpdateEndpoint = `${baseUrl}/building/build/update`; async function retrieveBuildList(payload: FilterListInterface) { const feedObject = new FilterList(payload).filter(); + console.log("feedObject", feedObject); const tokenResponse: any = await fetchDataWithToken( buildListEndpoint, feedObject, diff --git a/apicalls/checkEndpoint.tsx b/apicalls/checkEndpoint.tsx new file mode 100644 index 0000000..bc2f9f1 --- /dev/null +++ b/apicalls/checkEndpoint.tsx @@ -0,0 +1,22 @@ +"use server"; +import { fetchDataWithToken } from "./api-fetcher"; +import { baseUrl } from "./basics"; + +const accessAvailableEndpoint = `${baseUrl}/access/endpoint/available`; + +async function retrieveAvailableEndpoint(payload: string) { + const tokenResponse: any = await fetchDataWithToken( + accessAvailableEndpoint, + { + endpoint: payload, + }, + "POST", + false + ); + if (tokenResponse.status === 200) { + return true; + } + return false; +} + +export { retrieveAvailableEndpoint }; diff --git a/apicalls/cookies/token.tsx b/apicalls/cookies/token.tsx index 57b4d6b..8754198 100644 --- a/apicalls/cookies/token.tsx +++ b/apicalls/cookies/token.tsx @@ -105,7 +105,9 @@ async function retrieveUserSelection() { const avatarInfo = await retrieveAvatarInfo(); return { ...objectUserSelection, - lang: String(avatarInfo?.data?.lang).toLowerCase(), + lang: avatarInfo?.data?.lang + ? String(avatarInfo?.data?.lang).toLowerCase() + : undefined, avatar: avatarInfo?.data?.avatar, fullName: avatarInfo?.data?.full_name, }; diff --git a/apicalls/login/logout.tsx b/apicalls/login/logout.tsx index 51dbd75..c190208 100644 --- a/apicalls/login/logout.tsx +++ b/apicalls/login/logout.tsx @@ -11,6 +11,7 @@ interface LoginOutUser { } async function logoutActiveSession(payload: LoginOutUser) { + "use server"; const cookieStore = await cookies(); cookieStore.delete("accessToken"); cookieStore.delete("accessObject"); diff --git a/apicalls/test.tsx b/apicalls/test.tsx index 4f64d92..56919c4 100644 --- a/apicalls/test.tsx +++ b/apicalls/test.tsx @@ -52,10 +52,12 @@ export async function handleFormSubmission(formData: FormData): Promise { } else if (key.includes("section")) { } else if (key.includes("ACTION_ID")) { } else { - inputs.query = { - ...inputs.query, - [key]: value, - }; + if (value) { + inputs.query = { + ...inputs.query, + [key]: value, + }; + } } }); const queryEncrypt = await encryptQuery(inputs); @@ -63,3 +65,25 @@ export async function handleFormSubmission(formData: FormData): Promise { `/${formData.get("section")}?q=${queryEncrypt.replaceAll(" ", "+")}` ); } + +export async function handleCreateSubmission({ + section, + data, +}: { + section: string; + data: any; +}) { + const queryEncrypt = await encryptQuery(data); + redirect(`/${section}/create?q=${queryEncrypt.replaceAll(" ", "+")}`); +} + +export async function handleUpdateSubmission({ + section, + data, +}: { + section: string; + data: any; +}) { + const queryEncrypt = await encryptQuery(data); + +} diff --git a/apicalls/validations/validations.tsx b/apicalls/validations/validations.tsx index da7cf7d..222f44f 100644 --- a/apicalls/validations/validations.tsx +++ b/apicalls/validations/validations.tsx @@ -1,7 +1,6 @@ "use server"; import { fetchData, fetchDataWithToken } from "@/apicalls/api-fetcher"; import { baseUrl, cookieObject, tokenSecret } from "@/apicalls/basics"; - import { HeadersAndValidations } from "@/apicalls/validations/validationProcesser"; const headersAndValidationEndpoint = `${baseUrl}/validations/endpoint`; @@ -28,15 +27,14 @@ async function retrieveHeadersEndpoint({ endpoint }: EndpointInterface) { } return { status: selectResponse.status, - headers: {}, message: selectResponse.message, + headers: {}, }; } async function retrieveHeadersAndValidationByEndpoint({ endpoint, }: EndpointInterface) { - console.log("endpoint", endpoint); const selectResponse: any = await fetchDataWithToken( headersAndValidationEndpoint, { @@ -58,7 +56,6 @@ async function retrieveHeadersAndValidationByEndpoint({ return { status: selectResponse.status, message: selectResponse.message, - headers: null, validated: null, }; diff --git a/apimaps/building/pageInfo.ts b/apimaps/building/pageInfo.ts index cb0686d..8e3f3a5 100644 --- a/apimaps/building/pageInfo.ts +++ b/apimaps/building/pageInfo.ts @@ -2,6 +2,7 @@ const BuildPageInfo = { tr: [ { title: "Bina Listesi", + name: "table", icon: null, description: "Bina listeyebilirsiniz", endpoint: "/building/build/list", @@ -10,6 +11,7 @@ const BuildPageInfo = { { title: "Bina Ekle", icon: "BadgePlus", + name: "create", description: "Bina oluşturma sayfasına hoş geldiniz", endpoint: "/building/build/create", component: "AddCreate2Table", @@ -17,6 +19,7 @@ const BuildPageInfo = { { title: null, icon: "Pencil", + name: "update", description: "Bina güncelleme sayfasına hoş geldiniz", endpoint: "/building/build/update/{build_uu_id}", component: "AddUpdate2Table", diff --git a/apimaps/mappingApi.ts b/apimaps/mappingApi.ts index 5e95ca3..8470edd 100644 --- a/apimaps/mappingApi.ts +++ b/apimaps/mappingApi.ts @@ -18,18 +18,19 @@ import { BuildPageInfo, BuildAllEndpoints } from "./building/pageInfo"; const PagesInfosAndEndpoints = [ { + name: "BuildingPage", title: { tr: "Binalar", en: "Buildings", }, icon: "Hotel", - // component: "/build/page", - url: "/building?page=1", + url: "/building", pageInfo: BuildPageInfo, allEndpoints: BuildAllEndpoints, subCategories: BuildCategories, }, { + name: "", title: { tr: "Toplantılar", en: "Meetings", @@ -42,6 +43,7 @@ const PagesInfosAndEndpoints = [ subCategories: MeetingSubCategories, }, { + name: "", title: { tr: "Cari Hesaplar", en: "Accounts", @@ -54,6 +56,7 @@ const PagesInfosAndEndpoints = [ subCategories: AccountSubCategories, }, { + name: "", title: { tr: "Karar Defteri", en: "Decision Book", @@ -66,6 +69,7 @@ const PagesInfosAndEndpoints = [ subCategories: DecisionBookSubCategories, }, { + name: "", title: { tr: "Kimlikler", en: "Identities", @@ -78,6 +82,7 @@ const PagesInfosAndEndpoints = [ subCategories: IdentityCategories, }, { + name: "", title: { tr: "Erişilebilirlik", en: "Accessibility", @@ -90,6 +95,7 @@ const PagesInfosAndEndpoints = [ subCategories: AccesibleCategories, }, { + name: "", title: { tr: "Firmalar", en: "Companies", diff --git a/src/Icons/icons.tsx b/src/Icons/icons.tsx new file mode 100644 index 0000000..9488d6c --- /dev/null +++ b/src/Icons/icons.tsx @@ -0,0 +1,68 @@ +"use client"; +import { + Hotel, + Logs, + Landmark, + ScrollText, + UserPlus, + Cog, + Store, + BadgePlus, + Pencil, +} from "lucide-react"; +import { DoorOpen, TreePine, UsersRound } from "lucide-react"; +import { ClipboardList, ClipboardCheck } from "lucide-react"; +import { LucideLandmark } from "lucide-react"; +import { + Projector, + FolderKey, + FolderCog, + Stamp, + FolderCheck, +} from "lucide-react"; +import { PersonStanding, MapPinned, ScanSearch, Container } from "lucide-react"; +import { PackageCheck } from "lucide-react"; +import { + FolderOpenDot, + BriefcaseMedical, + Pickaxe, + BicepsFlexed, +} from "lucide-react"; + +const AllIcons = { + Hotel, + Logs, + Landmark, + ScrollText, + UserPlus, + Cog, + Store, + DoorOpen, + TreePine, + UsersRound, + ClipboardList, + ClipboardCheck, + LucideLandmark, + Projector, + FolderKey, + FolderCog, + Stamp, + FolderCheck, + PersonStanding, + MapPinned, + ScanSearch, + Container, + PackageCheck, + FolderOpenDot, + BriefcaseMedical, + Pickaxe, + BicepsFlexed, + BadgePlus, + Pencil, +}; + +function getIconByName(name: string) { + return Object.entries(AllIcons).find(([key]) => key === name)?.[1] ?? Pencil; +} + +export { AllIcons, getIconByName }; diff --git a/src/app/accounts/page.tsx b/src/app/accounts/page.tsx index bfc961a..a051f28 100644 --- a/src/app/accounts/page.tsx +++ b/src/app/accounts/page.tsx @@ -1,8 +1,8 @@ "use server"; import React from "react"; import { RefreshCcw } from "lucide-react"; -import Pagination from "./pagination"; -import TableComponent from "./table"; +import Pagination from "../../components/commons/pagination"; +import TableComponent from "../../components/commons/table"; import { decryptQuery, defaultPagination, @@ -11,16 +11,16 @@ import { import MainBodyWithHeader from "@/components/defaultLayout/MainBodyWithHeader"; import { redirect } from "next/navigation"; -const DashboardPage = async ({ searchParams }: { searchParams: any }) => { +const AccountsPage = async ({ searchParams }: { searchParams: any }) => { const searchParamsKeys = await searchParams; if (!searchParamsKeys?.q) { const defaultURL = await defaultPagination(); - console.log(defaultURL); redirect(`/accounts?q=${defaultURL}`); } const queryEncrypt = await decryptQuery( searchParamsKeys?.q.replace(/ /g, "+") ); + const accountPage = (

Dashboard

@@ -54,4 +54,4 @@ const DashboardPage = async ({ searchParams }: { searchParams: any }) => { return ; }; -export default DashboardPage; +export default AccountsPage; diff --git a/src/app/accounts/table.tsx b/src/app/accounts/table.tsx deleted file mode 100644 index 5babaef..0000000 --- a/src/app/accounts/table.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -interface TableComponentInterFace { - inputHeaders: any; -} -const TableComponent: React.FC = ({ - inputHeaders, -}) => { - return ( -
- {Object.entries(inputHeaders).map(([key, value]) => ( -
-

{key} :

- -
- ))} -
- ); -}; - -export default TableComponent; diff --git a/src/app/building/create/CreatePage.tsx b/src/app/building/create/CreatePage.tsx new file mode 100644 index 0000000..9e6836f --- /dev/null +++ b/src/app/building/create/CreatePage.tsx @@ -0,0 +1,104 @@ +"use client"; +import { RetrieveInputByType } from "@/hooks/renderInputWithValidation"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { convertApiValidationToZodValidation } from "@/lib/renderZodValidation"; + +interface CreatePageComponentInterface { + validator: any; + headers: any; +} + +const CreatePageComponent: React.FC = ({ + validator, + headers, +}) => { + const returnValidation = convertApiValidationToZodValidation(validator); + const { validSchemaZod, zodValidation, apiValidation } = returnValidation; + console.log("validSchemaZod", { + validSchemaZod, + zodValidation, + apiValidation, + validator, + headers, + }); + const form = useForm>({ + resolver: zodResolver(validSchemaZod), + defaultValues: {}, + }); + + function submitUpdate(formData: z.infer) { + // saveFunction({ + // uu_id: updateUUID, + // payload: validDataParser(formData), + // }).then((res: any) => { + // console.log(res); + // if (res?.status === 200) { + // } else { + // alert("Güncelleme başarısız"); + // } + // }); + } + + return ( + <> +
+
+ + {Object.entries(validator).map(([key, value]: [string, any]) => ( + { + return ( + + + {headers[key] || `Header not found ${key}`} + + + {RetrieveInputByType({ + type: value?.fieldType || "string", + props: { + className: "", + field: field, + placeholder: headers[key], + required: value?.required || false, + }, + })} + + {String(form.formState.errors[key]?.type) === + "invalid_type" ? ( + + "Lütfen metinsel bir değer giriniz" + + ) : ( + <> + )} + + ); + }} + /> + ))} + + + +
+ + ); +}; + +export default CreatePageComponent; diff --git a/src/app/building/create/page.tsx b/src/app/building/create/page.tsx new file mode 100644 index 0000000..745fa8c --- /dev/null +++ b/src/app/building/create/page.tsx @@ -0,0 +1,40 @@ +"use server"; + +import { retrieveAvailableEndpoint } from "@/apicalls/checkEndpoint"; +import { checkAccessTokenIsValid } from "@/apicalls/cookies/token"; +import { decryptQuery, defaultPagination } from "@/apicalls/test"; +import { retrieveHeadersAndValidationByEndpoint } from "@/apicalls/validations/validations"; +import { redirect } from "next/navigation"; +import CreatePageComponent from "./CreatePage"; + +export default async function BuildingCreatePage({ + searchParams, +}: { + searchParams: any; +}) { + if (!(await checkAccessTokenIsValid())) { + redirect("/login/email"); + } + + const buildKey = "building"; + const searchParamsKeys = await searchParams; + const endpointUrl = "/building/build/create"; + + const queryEncrypt = await decryptQuery(searchParamsKeys?.q); + const endpointAvailable = await retrieveAvailableEndpoint(endpointUrl); + const validateAndHeaders = await retrieveHeadersAndValidationByEndpoint({ + endpoint: endpointUrl, + }); + const validator = validateAndHeaders?.validated || {}; + const headers = validateAndHeaders?.headers || {}; + console.log("validateAndHeaders", validateAndHeaders); + console.log("endpointAvailable", endpointAvailable); + console.log("queryEncrypt", queryEncrypt); + return ( +
+

Create Building

+

{JSON.stringify(queryEncrypt)}

+ +
+ ); +} diff --git a/src/app/building/page.tsx b/src/app/building/page.tsx index 93373af..b4bfec9 100644 --- a/src/app/building/page.tsx +++ b/src/app/building/page.tsx @@ -1,10 +1,154 @@ "use server"; -import React from "react"; -import MainBodyWithHeader from "@/components/defaultLayout/MainBodyWithHeader"; -import BuildChildComponent from "@/pages/Build/Build"; +import React, { Suspense } from "react"; +import Link from "next/link"; +import { RefreshCcw, PlusCircle } from "lucide-react"; -const Page = () => { - return } />; +import MainBodyWithHeader from "@/components/defaultLayout/MainBodyWithHeader"; +import { + decryptQuery, + defaultPagination, + handleFormSubmission, +} from "@/apicalls/test"; +import { redirect } from "next/navigation"; +import TableComponent from "@/components/commons/table"; +import Pagination from "@/components/commons/pagination"; +import { + createBuild, + retrieveBuildList, + updateBuild, +} from "@/apicalls/building/build"; +import { retrieveHeadersAndValidationByEndpoint } from "@/apicalls/validations/validations"; +import { + checkAccessTokenIsValid, + retrieveUserSelection, +} from "@/apicalls/cookies/token"; +import { retrievePageInfoByComponentName } from "@/hooks/retrievePageInfoByComponentName"; +import { retrieveAvailableEndpoint } from "@/apicalls/checkEndpoint"; +import { checkPageAvaliable } from "@/hooks/checkpageAvaliable"; +import { logoutActiveSession } from "@/apicalls/login/logout"; + +const BuildinPage = async ({ searchParams }: { searchParams: any }) => { + const buildKey = "building"; + const pageName = "BuildingPage"; + const searchParamsKeys = await searchParams; + + if (!searchParamsKeys?.q) { + const defaultURL = await defaultPagination(); + redirect(`/${buildKey}?q=${defaultURL}`); + } + const queryEncrypt = await decryptQuery(searchParamsKeys?.q); + if (!(await checkAccessTokenIsValid())) { + redirect("/login/email"); + } + + const tableValues = { + endpoint: "building/build/list", + name: "table", + url: "/building", + function: retrieveBuildList, + data: [], + headers: {}, + validation: {}, + }; + const createValues = { + endpoint: "building/build/create", + name: "create", + url: "/building/create", + function: createBuild, + data: [], + headers: {}, + validation: {}, + }; + const updateValues = { + endpoint: "building/build/update/{build_uu_id}", + function: updateBuild, + name: "update", + url: "/building/update", + data: [], + headers: {}, + validation: {}, + }; + + let restrictions: any = { + update: updateValues, + create: createValues, + table: tableValues, + }; + + const user = await retrieveUserSelection(); + if (!user?.lang) { + await logoutActiveSession({ domain: "evyos.com.tr" }); + redirect("/login/email"); + } + const pageContent = await retrievePageInfoByComponentName( + pageName, + user?.lang + ); + const restrictionsChecked = await checkPageAvaliable({ + pageContent, + restrictions, + queryEncrypt, + }); + + if (!restrictionsChecked || !restrictionsChecked?.table) { + redirect("/home"); + } + + const BuildingPage = ( +
+

Dashboard

+
+
+

Welcome to your dashboard

+ {restrictionsChecked?.create && ( + + + Create + + )} +

{JSON.stringify(queryEncrypt)}

+ + {restrictionsChecked && ( + <> + + + + )} + + + +
+
+
+ ); + return ( + <> + Loading...
}> + + + + ); }; -export default Page; +export default BuildinPage; diff --git a/src/app/building/update/UpdatePage.tsx b/src/app/building/update/UpdatePage.tsx new file mode 100644 index 0000000..ff4cd34 --- /dev/null +++ b/src/app/building/update/UpdatePage.tsx @@ -0,0 +1,110 @@ +"use client"; +import { RetrieveInputByType } from "@/hooks/renderInputWithValidation"; +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { convertApiValidationToZodValidation } from "@/lib/renderZodValidation"; + +interface UpdatePageInterface { + validator: any; + headers: any; + queryEncrypt: any; +} + +const UpdatePageComponent: React.FC = ({ + validator, + headers, + queryEncrypt, +}) => { + const returnValidation = convertApiValidationToZodValidation(validator); + const { validSchemaZod, zodValidation, apiValidation } = returnValidation; + console.log("validSchemaZod", { + validSchemaZod, + zodValidation, + apiValidation, + validator, + headers, + queryEncrypt, + }); + const form = useForm>({ + resolver: zodResolver(validSchemaZod), + defaultValues: { + ...queryEncrypt, + }, + }); + + function submitUpdate(formData: z.infer) { + const updateUUID = queryEncrypt?.uu_id; + // saveFunction({ + // uu_id: updateUUID, + // payload: validDataParser(formData), + // }).then((res: any) => { + // console.log(res); + // if (res?.status === 200) { + // } else { + // alert("Güncelleme başarısız"); + // } + // }); + } + + return ( + <> +
+
+ + {Object.entries(validator).map(([key, value]: [string, any]) => ( + { + return ( + + + {headers[key] || `Header not found ${key}`} + + + {RetrieveInputByType({ + type: value?.fieldType || "string", + props: { + className: "", + field: field, + placeholder: headers[key], + required: value?.required || false, + }, + })} + + {String(form.formState.errors[key]?.type) === + "invalid_type" ? ( + + "Lütfen metinsel bir değer giriniz" + + ) : ( + <> + )} + + ); + }} + /> + ))} + + + +
+ + ); +}; + +export default UpdatePageComponent; diff --git a/src/app/building/update/page.tsx b/src/app/building/update/page.tsx new file mode 100644 index 0000000..eeaf986 --- /dev/null +++ b/src/app/building/update/page.tsx @@ -0,0 +1,63 @@ +"use server"; +import { updateBuild } from "@/apicalls/building/build"; +import { retrieveAvailableEndpoint } from "@/apicalls/checkEndpoint"; +import { checkAccessTokenIsValid } from "@/apicalls/cookies/token"; +import { decryptQuery, defaultPagination } from "@/apicalls/test"; +import { retrieveHeadersAndValidationByEndpoint } from "@/apicalls/validations/validations"; +import { redirect } from "next/navigation"; +import { RetrieveInputByType } from "@/hooks/renderInputWithValidation"; + +import React from "react"; +import UpdatePageComponent from "./UpdatePage"; + +export default async function BuildingUpdatePage({ + searchParams, +}: { + searchParams: any; +}) { + if (!(await checkAccessTokenIsValid())) { + redirect("/login/email"); + } + + const buildKey = "building/update"; + const searchParamsKeys = await searchParams; + const endpointUrl = "building/build/update/{build_uu_id}"; + if (!searchParamsKeys?.q) { + redirect(`/${buildKey}`); + } + const queryEncrypt = await decryptQuery(searchParamsKeys?.q); + const updateValues = { + endpoint: "building/build/update/{build_uu_id}", + function: updateBuild, + name: "update", + url: "/building/update", + data: [], + headers: {}, + validation: {}, + }; + + const endpointAvailable = await retrieveAvailableEndpoint(endpointUrl); + const validateAndHeaders = await retrieveHeadersAndValidationByEndpoint({ + endpoint: endpointUrl, + }); + const validator = validateAndHeaders?.validated || {}; + const headers = validateAndHeaders?.headers || {}; + console.log("endpointAvailable", endpointAvailable); + console.log("validator", validator); + console.log("headers", headers); + console.log("queryEncrypt", queryEncrypt); + + return ( +
+

Update Building

+

{JSON.stringify(queryEncrypt)}

+ + + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a23ac26..fd07a4b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -62,6 +62,29 @@ body { } } +.loading-container { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f5f5f5; +} + +.spinner { + width: 50px; + height: 50px; + border: 5px solid rgba(0, 0, 0, 0.1); + border-top: 5px solid #0070f3; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + @layer base { * { @apply border-border; diff --git a/src/app/accounts/pagination.tsx b/src/components/commons/pagination.tsx similarity index 100% rename from src/app/accounts/pagination.tsx rename to src/components/commons/pagination.tsx diff --git a/src/components/commons/table.tsx b/src/components/commons/table.tsx new file mode 100644 index 0000000..9c23aa6 --- /dev/null +++ b/src/components/commons/table.tsx @@ -0,0 +1,192 @@ +"use client"; +import React from "react"; +import { + useReactTable, + flexRender, + getCoreRowModel, + createColumnHelper, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableFooter, +} from "@/components/ui/table"; + +import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; +import { getIconByName } from "@/Icons/icons"; +import { useRouter } from "next/navigation"; +import { encryptQuery, handleUpdateSubmission } from "@/apicalls/test"; + +interface TableComponentInterFace { + restrictions: any; + query: any; +} + +const TableComponent: React.FC = ({ + restrictions, + query, +}) => { + const router = useRouter(); + const [updateRow, setUpdateRow] = React.useState(null); + const [columns, setColumns] = React.useState([]); + + const columnHelper = createColumnHelper(); + const table = useReactTable({ + data: restrictions.table?.data?.data || [], + columns, + getCoreRowModel: getCoreRowModel(), + }); + + React.useEffect(() => { + if (restrictions?.table?.headers) { + setColumns(createColumnsFromValidations(restrictions.table.headers)); + } + }, [restrictions.table.headers]); + + React.useEffect(() => { + if (updateRow) { + encryptQuery(updateRow).then((encryptData) => { + router.push(`/building/update?q=${encryptData.replaceAll(" ", "+")}`); + }); + } + }, [updateRow]); + + function createColumnsFromValidations(headers: any) { + const columns = Object.entries(headers).map(([key]: [string, any]) => { + return columnHelper.accessor(key, { + id: key, + footer: headers[key], + header: () => {headers[key]}, + cell: (info) => {info.getValue()}, + }); + }); + if (restrictions?.update) { + columns.push( + columnHelper.accessor("update", { + id: "update", + footer: "Update", + header: () => Update, + cell: () => ( +
+ {restrictions?.update.icon && + React.createElement(getIconByName(restrictions?.update.icon))} +
+ ), + }) + ); + } + return columns; + } + + return ( + <> +
+

{JSON.stringify(updateRow)}

+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+ {/* {header.id !== "update" && ( + changeOrderState(header.id)} + /> + )} */} + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + {/* {tableInfo.field === header.id && + header.id !== "update" && + (tableInfo.type.startsWith("a") ? ( + + ) : ( + + ))} */} +
+
+ ))} +
+ ))} +
+ + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => + cell.column.id !== "update" ? ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) : ( + setUpdateRow(row.original)} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + )} + + )) + ) : ( + + + No results. + + + )} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map( + (footer) => + footer.id !== "update" && ( + + {footer.isPlaceholder ? null : ( + + )} + + ) + )} + + ))} + +
+
+ + ); +}; + +export default TableComponent; diff --git a/src/components/ui/smart-datetime-input.tsx b/src/components/ui/smart-datetime-input.tsx new file mode 100644 index 0000000..e69aafb --- /dev/null +++ b/src/components/ui/smart-datetime-input.tsx @@ -0,0 +1,551 @@ +"use client"; + +import React from "react"; +import { parseDate } from "chrono-node"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ActiveModifiers } from "react-day-picker"; +import { Calendar, CalendarProps } from "@/components/ui/calendar"; +import { Input } from "@/components/ui/input"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Calendar as CalendarIcon, LucideTextCursorInput } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { format } from "date-fns"; + +/* -------------------------------------------------------------------------- */ +/* Inspired By: */ +/* @steventey */ +/* ------------------https://dub.co/blog/smart-datetime-picker--------------- */ +/* -------------------------------------------------------------------------- */ + +/** + * Utility function that parses dates. + * Parses a given date string using the `chrono-node` library. + * + * @param str - A string representation of a date and time. + * @returns A `Date` object representing the parsed date and time, or `null` if the string could not be parsed. + */ +export const parseDateTime = (str: Date | string) => { + if (str instanceof Date) return str; + return parseDate(str); +}; + +/** + * Converts a given timestamp or the current date and time to a string representation in the local time zone. + * format: `HH:mm`, adjusted for the local time zone. + * + * @param timestamp {Date | string} + * @returns A string representation of the timestamp + */ +export const getDateTimeLocal = (timestamp?: Date): string => { + const d = timestamp ? new Date(timestamp) : new Date(); + if (d.toString() === "Invalid Date") return ""; + return new Date(d.getTime() - d.getTimezoneOffset() * 60000) + .toISOString() + .split(":") + .slice(0, 2) + .join(":"); +}; + +/** + * Formats a given date and time object or string into a human-readable string representation. + * "MMM D, YYYY h:mm A" (e.g. "Jan 1, 2023 12:00 PM"). + * + * @param datetime - {Date | string} + * @returns A string representation of the date and time + */ +export const formatDateTime = (datetime: Date | string) => { + return new Date(datetime).toLocaleTimeString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + hour12: true, + }); +}; + +const inputBase = + "bg-transparent focus:outline-none focus:ring-0 focus-within:outline-none focus-within:ring-0 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50"; + +// @source: https://www.perplexity.ai/search/in-javascript-how-RfI7fMtITxKr5c.V9Lv5KA#1 +// use this pattern to validate the transformed date string for the natural language input +const naturalInputValidationPattern = + "^[A-Z][a-z]{2}sd{1,2},sd{4},sd{1,2}:d{2}s[AP]M$"; + +const DEFAULT_SIZE = 96; + +/** + * Smart time input Docs: {@link: https://shadcn-extension.vercel.app/docs/smart-time-input} + */ + +interface SmartDatetimeInputProps { + value?: Date; + onValueChange: (date: Date) => void; +} + +interface SmartDatetimeInputContextProps extends SmartDatetimeInputProps { + Time: string; + onTimeChange: (time: string) => void; +} + +const SmartDatetimeInputContext = + React.createContext(null); + +const useSmartDateInput = () => { + const context = React.useContext(SmartDatetimeInputContext); + if (!context) { + throw new Error( + "useSmartDateInput must be used within SmartDateInputProvider" + ); + } + return context; +}; + +export const SmartDatetimeInput = React.forwardRef< + HTMLInputElement, + Omit< + React.InputHTMLAttributes, + "type" | "ref" | "value" | "defaultValue" | "onBlur" + > & + SmartDatetimeInputProps +>(({ className, value, onValueChange, placeholder, disabled }, ref) => { + // ? refactor to be only used with controlled input + /* const [dateTime, setDateTime] = React.useState( + value ?? undefined + ); */ + + const [Time, setTime] = React.useState(""); + + const onTimeChange = React.useCallback((time: string) => { + setTime(time); + }, []); + + return ( + +
+
+ + +
+
+
+ ); +}); + +SmartDatetimeInput.displayName = "DatetimeInput"; + +// Make it a standalone component + +const TimePicker = () => { + const { value, onValueChange, Time, onTimeChange } = useSmartDateInput(); + const [activeIndex, setActiveIndex] = React.useState(-1); + const timestamp = 15; + + const formateSelectedTime = React.useCallback( + (time: string, hour: number, partStamp: number) => { + onTimeChange(time); + + const newVal = parseDateTime(value ?? new Date()); + + if (!newVal) return; + + newVal.setHours( + hour, + partStamp === 0 ? parseInt("00") : timestamp * partStamp + ); + + // ? refactor needed check if we want to use the new date + + onValueChange(newVal); + }, + [value] + ); + + const handleKeydown = React.useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (!document) return; + + const moveNext = () => { + const nextIndex = + activeIndex + 1 > DEFAULT_SIZE - 1 ? 0 : activeIndex + 1; + + const currentElm = document.getElementById(`time-${nextIndex}`); + + currentElm?.focus(); + + setActiveIndex(nextIndex); + }; + + const movePrev = () => { + const prevIndex = + activeIndex - 1 < 0 ? DEFAULT_SIZE - 1 : activeIndex - 1; + + const currentElm = document.getElementById(`time-${prevIndex}`); + + currentElm?.focus(); + + setActiveIndex(prevIndex); + }; + + const setElement = () => { + const currentElm = document.getElementById(`time-${activeIndex}`); + + if (!currentElm) return; + + currentElm.focus(); + + const timeValue = currentElm.textContent ?? ""; + + // this should work now haha that hour is what does the trick + + const PM_AM = timeValue.split(" ")[1]; + const PM_AM_hour = parseInt(timeValue.split(" ")[0].split(":")[0]); + const hour = + PM_AM === "AM" + ? PM_AM_hour === 12 + ? 0 + : PM_AM_hour + : PM_AM_hour === 12 + ? 12 + : PM_AM_hour + 12; + + const part = Math.floor( + parseInt(timeValue.split(" ")[0].split(":")[1]) / 15 + ); + + formateSelectedTime(timeValue, hour, part); + }; + + const reset = () => { + const currentElm = document.getElementById(`time-${activeIndex}`); + currentElm?.blur(); + setActiveIndex(-1); + }; + + switch (e.key) { + case "ArrowUp": + movePrev(); + break; + + case "ArrowDown": + moveNext(); + break; + + case "Escape": + reset(); + break; + + case "Enter": + setElement(); + break; + } + }, + [activeIndex, formateSelectedTime] + ); + + const handleClick = React.useCallback( + (hour: number, part: number, PM_AM: string, currentIndex: number) => { + formateSelectedTime( + `${hour}:${part === 0 ? "00" : timestamp * part} ${PM_AM}`, + hour, + part + ); + setActiveIndex(currentIndex); + }, + [formateSelectedTime] + ); + + const currentTime = React.useMemo(() => { + const timeVal = Time.split(" ")[0]; + return { + hours: parseInt(timeVal.split(":")[0]), + minutes: parseInt(timeVal.split(":")[1]), + }; + }, [Time]); + + React.useEffect(() => { + const getCurrentElementTime = () => { + const timeVal = Time.split(" ")[0]; + const hours = parseInt(timeVal.split(":")[0]); + const minutes = parseInt(timeVal.split(":")[1]); + const PM_AM = Time.split(" ")[1]; + + const formatIndex = + PM_AM === "AM" ? hours : hours === 12 ? hours : hours + 12; + const formattedHours = formatIndex; + + console.log(formatIndex); + + for (let j = 0; j <= 3; j++) { + const diff = Math.abs(j * timestamp - minutes); + const selected = + PM_AM === (formattedHours >= 12 ? "PM" : "AM") && + (minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp); + + if (selected) { + const trueIndex = + activeIndex === -1 ? formattedHours * 4 + j : activeIndex; + + setActiveIndex(trueIndex); + + const currentElm = document.getElementById(`time-${trueIndex}`); + currentElm?.scrollIntoView({ + block: "center", + behavior: "smooth", + }); + } + } + }; + + getCurrentElementTime(); + }, [Time, activeIndex]); + + const height = React.useMemo(() => { + if (!document) return; + const calendarElm = document.getElementById("calendar"); + if (!calendarElm) return; + return calendarElm.style.height; + }, []); + + return ( +
+

Time

+ +
    + {Array.from({ length: 24 }).map((_, i) => { + const PM_AM = i >= 12 ? "PM" : "AM"; + const formatIndex = i > 12 ? i % 12 : i === 0 || i === 12 ? 12 : i; + return Array.from({ length: 4 }).map((_, part) => { + const diff = Math.abs(part * timestamp - currentTime.minutes); + + const trueIndex = i * 4 + part; + + // ? refactor : add the select of the default time on the current device (H:MM) + const isSelected = + (currentTime.hours === i || + currentTime.hours === formatIndex) && + Time.split(" ")[1] === PM_AM && + (currentTime.minutes <= 53 + ? diff < Math.ceil(timestamp / 2) + : diff < timestamp); + + const isSuggested = !value && isSelected; + + const currentValue = `${formatIndex}:${ + part === 0 ? "00" : timestamp * part + } ${PM_AM}`; + + return ( +
  • handleClick(i, part, PM_AM, trueIndex)} + onFocus={() => isSuggested && setActiveIndex(trueIndex)} + > + {currentValue} +
  • + ); + }); + })} +
+
+
+ ); +}; + +const NaturalLanguageInput = React.forwardRef< + HTMLInputElement, + { + placeholder?: string; + disabled?: boolean; + } +>(({ placeholder, ...props }, ref) => { + const { value, onValueChange, Time, onTimeChange } = useSmartDateInput(); + + const _placeholder = placeholder ?? 'e.g. "tomorrow at 5pm" or "in 2 hours"'; + + const [inputValue, setInputValue] = React.useState(""); + + React.useEffect(() => { + const hour = new Date().getHours(); + const timeVal = `${ + hour >= 12 ? hour % 12 : hour + }:${new Date().getMinutes()} ${hour >= 12 ? "PM" : "AM"}`; + setInputValue(value ? formatDateTime(value) : ""); + onTimeChange(value ? Time : timeVal); + }, [value, Time]); + + const handleParse = React.useCallback( + (e: React.ChangeEvent) => { + // parse the date string when the input field loses focus + const parsedDateTime = parseDateTime(e.currentTarget.value); + if (parsedDateTime) { + const PM_AM = parsedDateTime.getHours() >= 12 ? "PM" : "AM"; + //fix the time format for this value + + const PM_AM_hour = parsedDateTime.getHours(); + + const hour = + PM_AM_hour > 12 + ? PM_AM_hour % 12 + : PM_AM_hour === 0 || PM_AM_hour === 12 + ? 12 + : PM_AM_hour; + + onValueChange(parsedDateTime); + setInputValue(formatDateTime(parsedDateTime)); + onTimeChange(`${hour}:${parsedDateTime.getMinutes()} ${PM_AM}`); + } + }, + [value] + ); + + const handleKeydown = React.useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case "Enter": + const parsedDateTime = parseDateTime(e.currentTarget.value); + if (parsedDateTime) { + const PM_AM = parsedDateTime.getHours() >= 12 ? "PM" : "AM"; + //fix the time format for this value + + const PM_AM_hour = parsedDateTime.getHours(); + + const hour = + PM_AM_hour > 12 + ? PM_AM_hour % 12 + : PM_AM_hour === 0 || PM_AM_hour === 12 + ? 12 + : PM_AM_hour; + + onValueChange(parsedDateTime); + setInputValue(formatDateTime(parsedDateTime)); + onTimeChange(`${hour}:${parsedDateTime.getMinutes()} ${PM_AM}`); + } + break; + } + }, + [value] + ); + + return ( + setInputValue(e.currentTarget.value)} + onKeyDown={handleKeydown} + onBlur={handleParse} + className={cn("px-2 mr-0.5 flex-1 border-none h-8 rounded", inputBase)} + {...props} + /> + ); +}); + +NaturalLanguageInput.displayName = "NaturalLanguageInput"; + +type DateTimeLocalInputProps = {} & CalendarProps; + +const DateTimeLocalInput = ({ + className, + ...props +}: DateTimeLocalInputProps) => { + const { value, onValueChange, Time } = useSmartDateInput(); + + const formateSelectedDate = React.useCallback( + ( + date: Date | undefined, + selectedDate: Date, + m: ActiveModifiers, + e: React.MouseEvent + ) => { + const parsedDateTime = parseDateTime(selectedDate); + + if (parsedDateTime) { + parsedDateTime.setHours( + parseInt(Time.split(":")[0]), + parseInt(Time.split(":")[1]) + ); + onValueChange(parsedDateTime); + } + }, + [value, Time] + ); + + return ( + + + + + +
+ + +
+
+
+ ); +}; + +DateTimeLocalInput.displayName = "DateTimeLocalInput"; diff --git a/src/hooks/checkpageAvaliable.tsx b/src/hooks/checkpageAvaliable.tsx new file mode 100644 index 0000000..efa245c --- /dev/null +++ b/src/hooks/checkpageAvaliable.tsx @@ -0,0 +1,55 @@ +"use server"; +import { retrieveAvailableEndpoint } from "@/apicalls/checkEndpoint"; +import { retrieveHeadersAndValidationByEndpoint } from "@/apicalls/validations/validations"; + +async function checkPageAvaliable({ + pageContent, + restrictions, + queryEncrypt, +}: { + pageContent: any; + restrictions: any; + queryEncrypt: any; +}) { + let restrictionsList: any = { + table: { + data: [], + headers: {}, + validation: {}, + }, + update: { + data: [], + headers: {}, + validation: {}, + }, + create: { + data: [], + headers: {}, + validation: {}, + }, + }; + await Promise.all( + pageContent?.map(async (listItem: any) => { + const { endpoint, name } = listItem; + const endpointAvailable = await retrieveAvailableEndpoint(endpoint); + if (endpointAvailable) { + if (listItem?.name === "table") { + restrictionsList[name].data = await restrictions[name].function( + queryEncrypt || {} + ); + } + const validateAndHeaders = await retrieveHeadersAndValidationByEndpoint( + { + endpoint: listItem?.endpoint, + } + ); + restrictionsList[name].headers = validateAndHeaders?.headers; + restrictionsList[name].validation = validateAndHeaders?.validated; + restrictionsList[name].icon = listItem?.icon; + } + }) ?? [] + ); + return restrictionsList; +} + +export { checkPageAvaliable }; diff --git a/src/hooks/initializeEndpoint.tsx b/src/hooks/initializeEndpoint.tsx new file mode 100644 index 0000000..8656a78 --- /dev/null +++ b/src/hooks/initializeEndpoint.tsx @@ -0,0 +1,71 @@ +"use client"; +import { retrieveUserSelection } from "@/apicalls/cookies/token"; +import { retrieveHeadersAndValidationByEndpoint } from "@/apicalls/validations/validations"; +import { AvailableLanguages } from "@/apimaps/mappingApi"; +import { checkEndpointAvailability } from "@/apimaps/mappingApiFunctions"; +import { retrievePageInfoByComponentName } from "./retrievePageInfoByComponentName"; + +async function initializePageRequirements( + endpoint: string, + pageInfoFromApi: Array<{ endpoint: string; [key: string]: any }>, + setFunction: Function, + setterFunction: Function +): Promise { + try { + const validation = await retrieveHeadersAndValidationByEndpoint({ + endpoint: endpoint, + }); + const pageInfo = pageInfoFromApi.find( + (page: { endpoint: string }) => page.endpoint === endpoint + ); + + if (pageInfo && validation.status === 200) { + setFunction({ + ...pageInfo, + validation: validation.validated, + headers: validation.headers, + setterFunction: setterFunction, + }); + } + } catch (error) { + console.error(`Error initializing endpoint ${endpoint}:`, error); + } +} + +interface Mapper { + setFunction: Function; + setterFunction: Function; +} + +async function initializePageContent( + pageName: string, + eventsAvailable: any, + MappingBuild: Record +): Promise { + try { + const user = await retrieveUserSelection(); + if (!AvailableLanguages.includes((user?.lang as string) || "")) { + new Error("Language not available"); + } + + const pageContent = retrievePageInfoByComponentName(pageName, user?.lang); + if (!Array.isArray(pageContent)) return; + await Promise.all( + Object.entries(MappingBuild).map(async ([endpoint, mapper]) => { + const { setFunction, setterFunction } = mapper as Mapper; + if (checkEndpointAvailability(endpoint, eventsAvailable)) { + await initializePageRequirements( + endpoint, + pageContent, + setFunction, + setterFunction + ); + } + }) + ); + } catch (error) { + console.error("Error initializing page content:", error); + } +} + +export { initializePageRequirements, initializePageContent }; diff --git a/src/hooks/renderInputWithValidation.tsx b/src/hooks/renderInputWithValidation.tsx new file mode 100644 index 0000000..4653cfe --- /dev/null +++ b/src/hooks/renderInputWithValidation.tsx @@ -0,0 +1,113 @@ +"use client"; +import React from "react"; +import { Input } from "@/components/ui/input"; +import { SmartDatetimeInput } from "@/components/ui/smart-datetime-input"; +import { cn } from "@/lib/utils"; + +type ValidationTypes = "string" | "integer" | "datetime" | "boolean"; +interface InputProps { + field: any; + required: boolean; + className?: string; + placeholder?: string; +} +const StringInput = ({ + className, + placeholder, + field, + required, +}: InputProps) => { + return ( + <> + + + ); +}; + +const NumberInput = ({ + className, + placeholder, + field, + required, +}: InputProps) => { + return ( + <> + + + ); +}; + +const BooleanInput = ({ + className, + placeholder, + field, + required, +}: InputProps) => { + return ( + <> + + + ); +}; + +const DatetimeInput = ({ + className, + placeholder, + field, + required, +}: InputProps) => { + return ( + <> +

+ + + ); +}; + +function RetrieveInputByType({ + type, + props, +}: { + type: ValidationTypes; + props: InputProps; +}) { + switch (type) { + case "integer": + return ; + case "datetime": + return ; + case "boolean": + return ; + default: + return ; + } +} + +export { RetrieveInputByType }; diff --git a/src/hooks/retrievePageContent.ts b/src/hooks/retrievePageContent.ts new file mode 100644 index 0000000..80490c7 --- /dev/null +++ b/src/hooks/retrievePageContent.ts @@ -0,0 +1,28 @@ +import { + LanguagesInterface, + PagesInfosAndEndpoints, +} from "@/apimaps/mappingApi"; + +const retrievePageContent = ( + pageName: string, + lang: keyof LanguagesInterface +) => { + return ( + PagesInfosAndEndpoints.find((page) => page.component === pageName) + ?.pageInfo?.[lang] || null + ); +}; + +const retrievepageInfoOfEndpoint = ( + pageName: string, + endpoint: string, + lang: string +) => { + const pageContent = retrievePageContent( + pageName, + lang as keyof LanguagesInterface + ); + return pageContent?.find((page) => page.endpoint === endpoint); +}; + +export { retrievePageContent, retrievepageInfoOfEndpoint }; diff --git a/src/hooks/retrievePageInfoByComponentName.tsx b/src/hooks/retrievePageInfoByComponentName.tsx new file mode 100644 index 0000000..65f4fac --- /dev/null +++ b/src/hooks/retrievePageInfoByComponentName.tsx @@ -0,0 +1,30 @@ +import { PagesInfosAndEndpoints } from "@/apimaps/mappingApi"; + +const retrievePageInfoByComponentName = ( + componentName: string, + lang: string +) => { + const searchInCategory = (category: any): any => { + if (category.name === componentName) { + return category.pageInfo?.[lang]; + } + if (category.subCategories) { + for (const subCategory in category.subCategories) { + const result = searchInCategory(category.subCategories[subCategory]); + if (result) { + return result.pageInfo?.[lang]; + } + } + } + }; + + for (const category in PagesInfosAndEndpoints) { + const result = searchInCategory(PagesInfosAndEndpoints[category]); + if (result) { + return result; + } + } + return null; +}; + +export { retrievePageInfoByComponentName }; diff --git a/src/hooks/retrievePagination.tsx b/src/hooks/retrievePagination.tsx new file mode 100644 index 0000000..abb5dc5 --- /dev/null +++ b/src/hooks/retrievePagination.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +interface TableInfo { + totalPages: number; + currentPage: number; + size: number; + field: string; + type: string; +} + +interface PaginationResult { + response: any; + setTableInfo: React.Dispatch>; + checkPaginationState: React.Dispatch>; +} + +interface TableInfoState { + size: number; + currentPage: number; + field: string; + type: string; + totalPages: number; + previousAvailable: boolean; + nextAvailable: boolean; + query: Record; +} + +const useRetrievePagination = ({ + response, + setTableInfo, + checkPaginationState, +}: PaginationResult) => { + const paginationRes = response?.pagination; + const pagesTotal = paginationRes?.["page/total_page"]; + const sizeAndPage = paginationRes?.["size/total_count"]; + setTableInfo((prev) => ({ + ...prev, + totalPages: Number(pagesTotal[1]), + currentPage: Number(pagesTotal[0]), + size: Number(sizeAndPage[0]), + // field: String(paginationRes.order_field), + // type: String(paginationRes.order_type), + })); + checkPaginationState({ + currentPage: Number(pagesTotal[0]), + totalPages: Number(pagesTotal[1]), + }); +}; + +export { useRetrievePagination }; diff --git a/src/hooks/setComponentAsPage.tsx b/src/hooks/setComponentAsPage.tsx new file mode 100644 index 0000000..92ed3e7 --- /dev/null +++ b/src/hooks/setComponentAsPage.tsx @@ -0,0 +1,41 @@ +"use client"; +import React from "react"; +import { getPage } from "@/pages/DynamicPages/allPages"; + +function setComponentAsPageIfAvailable( + componentName: string, + eventsAvailable: any, + setPageFunction: React.Dispatch> +) { + const component = getPage(componentName); + if (component.props) { + const PageComponent = React.createElement<{ + eventsAvailable: any; + pageSetterFunction: React.Dispatch>; + }>(component.component as any, { + eventsAvailable: eventsAvailable, + pageSetterFunction: setPageFunction, + }); + setPageFunction(PageComponent); + } else { + setPageFunction(null); + } +} + +function setComponentAsPage( + componentName: string, + props: any, + setPageFunction: React.Dispatch> +) { + const component = getPage(componentName); + if (props) { + const PageComponent = React.createElement(component.component, { + ...props, + }); + setPageFunction(PageComponent); + } else { + setPageFunction(null); + } +} + +export { setComponentAsPage, setComponentAsPageIfAvailable }; diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts new file mode 100644 index 0000000..0791aa6 --- /dev/null +++ b/src/hooks/useClickOutside.ts @@ -0,0 +1,35 @@ +import { useEffect, useRef } from "react"; + +export const useClickOutside = (handler: () => void) => { + const ref = useRef(null); + const isMouseDownOutside = useRef(false); + + useEffect(() => { + const handleMouseDown = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + isMouseDownOutside.current = true; + } + }; + + const handleMouseUp = (e: MouseEvent) => { + if ( + isMouseDownOutside.current && + ref.current && + !ref.current.contains(e.target as Node) + ) { + handler(); + } + isMouseDownOutside.current = false; + }; + + document.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousedown", handleMouseDown); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [handler]); + + return ref; +};