diff --git a/ServicesWeb/customer/src/app/(AuthLayout)/auth/[...page]/page.tsx b/ServicesWeb/customer/src/app/(AuthLayout)/auth/[...page]/page.tsx new file mode 100644 index 0000000..686868b --- /dev/null +++ b/ServicesWeb/customer/src/app/(AuthLayout)/auth/[...page]/page.tsx @@ -0,0 +1,19 @@ +'use server'; +import { AuthLayout } from "@/layouts/auth/layout"; +import { AuthServerProps } from "@/validations/mutual/pages/props"; +import { LanguageTypes } from "@/validations/mutual/language/validations"; +import { checkAccessTokenIsValid } from "@/fetchers/mutual/cookies/token"; +import { getOnlineFromRedis } from "@/fetchers/custom/context/page/online/fetch"; +import { redirect } from "next/navigation"; +import Login from "@/pages/single/auth/login/page"; + +const AuthPageSSR = async ({ params, searchParams }: AuthServerProps) => { + const awaitedParams = await params; + const awaitedSearchParams = await searchParams; + const pageUrlFromParams = `/${awaitedParams.page?.join("/")}` || "/login"; + const tokenValid = await checkAccessTokenIsValid(); + try { const online = await getOnlineFromRedis(); if (tokenValid && online) { redirect("/panel/dashboard") } } catch (error) { } + return
} activePageUrl={pageUrlFromParams} />
+} + +export default AuthPageSSR; diff --git a/ServicesWeb/customer/src/app/(DashboardLayout)/panel/[...page]/page.tsx b/ServicesWeb/customer/src/app/(DashboardLayout)/panel/[...page]/page.tsx new file mode 100644 index 0000000..31661d8 --- /dev/null +++ b/ServicesWeb/customer/src/app/(DashboardLayout)/panel/[...page]/page.tsx @@ -0,0 +1,15 @@ +'use server'; +import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props"; +import { DashboardLayout } from "@/layouts/dashboard/layout"; +import { redirect } from "next/navigation"; +import { checkAccessTokenIsValid } from "@/fetchers/mutual/cookies/token"; + +const MainEnPage: React.FC = async ({ params, searchParams }) => { + const parameters = await params; + const searchParameters = await searchParams; + const tokenValid = await checkAccessTokenIsValid() + if (!tokenValid) { redirect("/auth/login") } + return
+} + +export default MainEnPage; diff --git a/ServicesWeb/customer/src/app/api/login/email/route.ts b/ServicesWeb/customer/src/app/api/login/email/route.ts index 15e5b5a..ad9d6d9 100644 --- a/ServicesWeb/customer/src/app/api/login/email/route.ts +++ b/ServicesWeb/customer/src/app/api/login/email/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { loginViaAccessKeys } from "@/apifetchers/custom/login/login"; -import { loginSchemaEmail } from "@/webPages/auth/login/schemas"; +import { loginViaAccessKeys } from "@/fetchers/custom/login/login"; +import { loginSchemaEmail } from "@/pages/single/auth/login/schemas"; export async function POST(req: Request): Promise { try { diff --git a/ServicesWeb/customer/src/app/dashboard/page.tsx b/ServicesWeb/customer/src/app/dashboard/page.tsx index 462301f..869f2ea 100644 --- a/ServicesWeb/customer/src/app/dashboard/page.tsx +++ b/ServicesWeb/customer/src/app/dashboard/page.tsx @@ -1,7 +1,7 @@ import { DashboardLayout } from '@/layouts/dashboard/layout'; import { LanguageTypes } from '@/validations/mutual/language/validations'; -export default function Page({ params, searchParams }: { params: { page?: string[] }, searchParams: Record }) { +export default async function Page({ params, searchParams }: { params: { page?: string[] }, searchParams: Record }) { const lang: LanguageTypes = 'en'; - return ; + return } diff --git a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx index d5a43be..926d3bb 100644 --- a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx +++ b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendMulti.tsx @@ -1,6 +1,6 @@ import { ContentProps } from "@/validations/mutual/dashboard/props"; import ContentToRenderNoPage from "@/pages/mutual/noContent/page"; -import pageIndexMulti from "@/pages/multi/index"; +import { pageIndexMulti } from "@/pages/multi/index" const PageToBeChildrendMulti: React.FC = ({ activePageUrl, diff --git a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx b/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx deleted file mode 100644 index 4cf93a8..0000000 --- a/ServicesWeb/customer/src/components/custom/content/PageToBeChildrendSingle.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ContentProps } from "@/validations/mutual/dashboard/props"; -import ContentToRenderNoPage from "@/pages/mutual/noContent/page"; -import { resolveWhichPageToRenderSingle } from "@/pages/resolver/resolver"; - -const PageToBeChildrendSingle: React.FC = async ({ lang, translations, activePageUrl, mode }) => { - const ApplicationToRender = await resolveWhichPageToRenderSingle({ activePageUrl }) - if (ApplicationToRender) { - return - } - return -} - -export default PageToBeChildrendSingle diff --git a/ServicesWeb/customer/src/components/custom/footer/component.tsx b/ServicesWeb/customer/src/components/custom/footer/component.tsx index fbdd45b..73c2dd3 100644 --- a/ServicesWeb/customer/src/components/custom/footer/component.tsx +++ b/ServicesWeb/customer/src/components/custom/footer/component.tsx @@ -23,7 +23,7 @@ const FooterComponent: FC = ({ const lang = onlineData?.lang as LanguageTypes || 'en'; return ( -
+
{!configLoading && configData && ( diff --git a/ServicesWeb/customer/src/components/custom/header/component.tsx b/ServicesWeb/customer/src/components/custom/header/component.tsx index 0c14b3c..0a55458 100644 --- a/ServicesWeb/customer/src/components/custom/header/component.tsx +++ b/ServicesWeb/customer/src/components/custom/header/component.tsx @@ -1,5 +1,5 @@ 'use client'; -import { FC } from "react"; +import { FC, useState, useEffect } from "react"; import { HeaderProps } from "@/validations/mutual/dashboard/props"; import { langGetKey } from "@/lib/langGet"; import { LanguageTypes } from "@/validations/mutual/language/validations"; @@ -8,35 +8,208 @@ import LanguageSelectionComponent from "@/components/mutual/languageSelection/co const translations = { en: { selectedPage: "selectedPage", - page: "page" + page: "page", + search: "Search...", + notifications: "Notifications", + messages: "Messages", + profile: "Profile", + settings: "Settings", + logout: "Log Out" }, tr: { selectedPage: "seçiliSayfa", - page: "sayfa" + page: "sayfa", + search: "Ara...", + notifications: "Bildirimler", + messages: "Mesajlar", + profile: "Profil", + settings: "Ayarlar", + logout: "Çıkış" } } const HeaderComponent: FC = ({ - activePageUrl, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline, + activePageUrl, searchParams, + onlineData, onlineLoading, onlineError, refreshOnline, updateOnline, userData, userLoading, userError, refreshUser, updateUser }) => { const lang = onlineData?.lang as LanguageTypes || 'en'; + const [activeTab, setActiveTab] = useState('notifications'); + const [isFullscreen, setIsFullscreen] = useState(false); + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); + + const toggleFullscreen = () => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen().catch(err => { + console.error(`Error attempting to enable fullscreen: ${err.message}`); + }); + } + }; + return ( -
-
-

{langGetKey(translations[lang], 'selectedPage')} :

-

{activePageUrl || langGetKey(translations[lang], 'page')}

+
+ + +
+

{activePageUrl || langGetKey(translations[lang], 'page')}

-
- {!onlineLoading && onlineData && onlineData.userType && ( -
- {lang} - {onlineData.userType} + +
    +
  • + +
    +
    +
    + + +
    +
    - )} -
+ + +
  • + +
    +
    + + +
    +
    + + +
    +
    +
  • + + + +
  • + + +
  • + +
    + +
    +
    ); }; diff --git a/ServicesWeb/customer/src/fetchers/base.ts b/ServicesWeb/customer/src/fetchers/base.ts index f32899a..e10c496 100644 --- a/ServicesWeb/customer/src/fetchers/base.ts +++ b/ServicesWeb/customer/src/fetchers/base.ts @@ -1,11 +1,12 @@ -import { ApiResponse, CookieObject } from "./types"; +import { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies"; +import { ApiResponse } from "./types"; import NextCrypto from "next-crypto"; const tokenSecretEnv = process.env.TOKENSECRET_90; const tokenSecret = tokenSecretEnv || "e781d1b0-9418-40b3-9940-385abf81a0b7"; const REDIS_TIMEOUT = 5000; // Redis operation timeout (5 seconds) const nextCrypto = new NextCrypto(tokenSecret); -const cookieObject: CookieObject = { +const cookieObject: Partial = { httpOnly: true, path: "/", sameSite: "none", diff --git a/ServicesWeb/customer/src/fetchers/custom/login/login.tsx b/ServicesWeb/customer/src/fetchers/custom/login/login.tsx new file mode 100644 index 0000000..96d7212 --- /dev/null +++ b/ServicesWeb/customer/src/fetchers/custom/login/login.tsx @@ -0,0 +1,200 @@ +"use server"; +import NextCrypto from "next-crypto"; +import { fetchData, fetchDataWithToken } from "@/fetchers/fecther"; +import { cookieObject, tokenSecret } from "@/fetchers/base"; +import { urlLoginEndpoint, urlLoginSelectEndpoint, urlLogoutEndpoint } from "@/fetchers/index"; + +import { cookies } from "next/headers"; +import { setNewCompleteToRedis, getCompleteFromRedis } from "@/fetchers/custom/context/complete/fetch"; +import { + defaultValuesHeader, + defaultValuesMenu, + defaultValuesOnline, + defaultValuesPageConfig, +} from "@/fetchers/types/context"; +import { retrievePageList } from "@/fetchers/mutual/cookies/token"; +import { deleteAllCookies } from "@/fetchers/mutual/cookies/cookie-actions"; +import { setMenuToRedis } from "@/fetchers/custom/context/page/menu/fetch"; +import { LoginViaAccessKeys, LoginSelect, LoginSelectOccupant } from "@/fetchers/types/login/validations"; + +async function logoutActiveSession() { + const response = await fetchDataWithToken(urlLogoutEndpoint, {}, "GET", false); + await deleteAllCookies(); + return response; +} + +async function initRedis(loginRespone: any, firstSelection: any, accessToken: string, redisKey: string) { + let alreadyAtRedis = null + try { + const redisData = await getCompleteFromRedis(); + alreadyAtRedis = redisData; + } catch (error) { } + if (!alreadyAtRedis) { + const loginObjectToRedis = { + online: { + ...defaultValuesOnline, + lastLogin: new Date(), + userType: `${loginRespone.user_type}`.toUpperCase(), + lang: loginRespone.user.person.country_code.toLowerCase(), + }, + pageConfig: defaultValuesPageConfig, + menu: defaultValuesMenu, + header: defaultValuesHeader, + selection: { selectionList: loginRespone.selection_list, activeSelection: firstSelection }, + user: loginRespone.user, + settings: { lastOnline: new Date(), token: accessToken }, + chatRoom: [], + notifications: [], + messages: [], + } + await setNewCompleteToRedis(loginObjectToRedis, redisKey); + } +} + +async function loginViaAccessKeys(payload: LoginViaAccessKeys) { + const cookieStore = await cookies(); + try { + const response = await fetchData( + urlLoginEndpoint, + { + access_key: payload.accessKey, + password: payload.password, + remember_me: payload.rememberMe, + }, + "POST", + false + ); + await deleteAllCookies() + if (response.status === 200 || response.status === 202) { + + const loginRespone: any = response?.data; + const firstSelection = loginRespone.selection_list.find((item: any) => item.uu_id === loginRespone.selection_list[0].uu_id); + + const nextCrypto = new NextCrypto(tokenSecret); + const accessToken = await nextCrypto.encrypt(loginRespone.access_token); + + const redisKey = `CLIENT:${loginRespone.user_type.toUpperCase()}:${firstSelection.uu_id}`; + const redisKeyAccess = await nextCrypto.encrypt(redisKey); + const usersSelection = await nextCrypto.encrypt( + JSON.stringify({ + selected: firstSelection.uu_id, + userType: loginRespone.user_type.toUpperCase(), + redisKey, + }) + ); + + await initRedis(loginRespone, firstSelection, accessToken, redisKey); + cookieStore.set({ + name: "eys-zzz", + value: accessToken, + ...cookieObject, + }); + cookieStore.set({ + name: "eys-yyy", + value: redisKeyAccess, + ...cookieObject, + }); + cookieStore.set({ + name: "eys-sel", + value: usersSelection, + ...cookieObject, + }); + try { + return { + completed: true, + message: "Login successful", + status: 200, + data: loginRespone, + }; + } catch (error) { + console.error("JSON parse error:", error); + return { + completed: false, + message: "Login NOT successful", + status: 401, + data: "{}", + }; + } + } + + return { + completed: false, + // error: response.error || "Login failed", + // message: response.message || "Authentication failed", + status: response.status || 500, + }; + } catch (error) { + console.error("Login error:", error); + return { + completed: false, + // error: error instanceof Error ? error.message : "Login error", + // message: "An error occurred during login", + status: 500, + }; + } +} + +async function loginSelectEmployee(payload: LoginSelect) { + const cookieStore = await cookies(); + const nextCrypto = new NextCrypto(tokenSecret); + const employeeUUID = payload.uuid; + const redisKey = `CLIENT:EMPLOYEE:${employeeUUID}`; + const selectResponse: any = await fetchDataWithToken(urlLoginSelectEndpoint, { uuid: employeeUUID }, "POST", false); + cookieStore.delete({ name: "eys-sel", ...cookieObject }); + + if (selectResponse.status === 200 || selectResponse.status === 202) { + const usersSelection = await nextCrypto.encrypt( + JSON.stringify({ + selected: employeeUUID, + userType: "employee", + redisKey, + }) + ); + cookieStore.set({ + name: "eys-sel", + value: usersSelection, + ...cookieObject, + }); + try { + const pageList = await retrievePageList() + await setMenuToRedis({ + selectionList: pageList, + activeSelection: "/dashboard" + }) + } catch (error) { } + } + return selectResponse; +} + +async function loginSelectOccupant(payload: LoginSelectOccupant) { + const livingSpaceUUID = payload.uuid; + const cookieStore = await cookies(); + const nextCrypto = new NextCrypto(tokenSecret); + + const selectResponse: any = await fetchDataWithToken(urlLoginSelectEndpoint, { uuid: livingSpaceUUID }, "POST", false); + const redisKey = `CLIENT:OCCUPANT:${livingSpaceUUID}`; + cookieStore.delete("eys-sel"); + + if (selectResponse.status === 200 || selectResponse.status === 202) { + const usersSelection = await nextCrypto.encrypt( + JSON.stringify({ + selected: livingSpaceUUID, + userType: "OCCUPANT", + redisKey, + }) + ); + cookieStore.set({ + name: "eys-sel", + value: usersSelection, + ...cookieObject, + }); + } + return selectResponse; +} + +export { + loginViaAccessKeys, + loginSelectEmployee, + loginSelectOccupant, + logoutActiveSession, +}; diff --git a/ServicesWeb/customer/src/fetchers/index.ts b/ServicesWeb/customer/src/fetchers/index.ts index 96a0044..e291ba7 100644 --- a/ServicesWeb/customer/src/fetchers/index.ts +++ b/ServicesWeb/customer/src/fetchers/index.ts @@ -25,12 +25,19 @@ const urlCheckToken = `${baseUrlAuth}/authentication/token/check`; const urlPageValid = `${baseUrlRestriction}/restrictions/page/valid`; const urlSiteUrls = `${baseUrlRestriction}/restrictions/sites/list`; +const urlLoginEndpoint = `${baseUrlAuth}/authentication/login`; +const urlLoginSelectEndpoint = `${baseUrlAuth}/authentication/select`; +const urlLogoutEndpoint = `${baseUrlAuth}/authentication/logout`; + const urlTesterList = `${baseUrlTester}/tester/list`; export { urlCheckToken, urlPageValid, urlSiteUrls, + urlLoginEndpoint, + urlLoginSelectEndpoint, + urlLogoutEndpoint, // For test use only urlTesterList, }; diff --git a/ServicesWeb/customer/src/fetchers/types/login/validations.ts b/ServicesWeb/customer/src/fetchers/types/login/validations.ts new file mode 100644 index 0000000..d634ee9 --- /dev/null +++ b/ServicesWeb/customer/src/fetchers/types/login/validations.ts @@ -0,0 +1,15 @@ +interface LoginViaAccessKeys { + accessKey: string; + password: string; + rememberMe: boolean; +} + +interface LoginSelect { + uuid: string; +} + +interface LoginSelectOccupant { + uuid: any; +} + +export type { LoginViaAccessKeys, LoginSelect, LoginSelectOccupant }; diff --git a/ServicesWeb/customer/src/pages/multi/index.ts b/ServicesWeb/customer/src/pages/multi/index.ts index 3e5ff0c..f9c65c5 100644 --- a/ServicesWeb/customer/src/pages/multi/index.ts +++ b/ServicesWeb/customer/src/pages/multi/index.ts @@ -1,10 +1,10 @@ -import { DashboardPage } from '@/components/custom/content/DashboardPage'; +import { DashboardPage } from "@/components/custom/content/DashboardPage"; const pageIndexMulti: Record>> = { - '/dashboard': { - 'DashboardPage': DashboardPage - }, - // Add more pages as needed + "/dashboard": { + DashboardPage: DashboardPage, + }, + // Add more pages as needed }; -export default pageIndexMulti; +export { pageIndexMulti }; diff --git a/ServicesWeb/customer/src/pages/single/a.txt b/ServicesWeb/customer/src/pages/single/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ServicesWeb/customer/src/pages/single/auth/login/hook.ts b/ServicesWeb/customer/src/pages/single/auth/login/hook.ts new file mode 100644 index 0000000..2532836 --- /dev/null +++ b/ServicesWeb/customer/src/pages/single/auth/login/hook.ts @@ -0,0 +1,37 @@ +import { LanguageTypes } from "@/validations/mutual/language/validations"; + +export function loginHook( + startTransition: any, + data: any, + setError: any, + setJsonText: any, + Router: any, + lang: LanguageTypes +) { + try { + const sendData = { ...data }; + startTransition(() => { + fetch("/api/login/email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(sendData), + }) + .then((response) => { + if (response.status === 200) { + response.json().then((data) => { + setTimeout(() => { + Router.push(`/panel/dashboard`); + }, 100); + }); + } else { + response.json().then((data) => { + setError(data?.message); + }); + } + }) + .catch(() => {}); + }); + } catch (error) { + setError("An error occurred during login"); + } +} diff --git a/ServicesWeb/customer/src/pages/single/auth/login/language.ts b/ServicesWeb/customer/src/pages/single/auth/login/language.ts new file mode 100644 index 0000000..0c2dddb --- /dev/null +++ b/ServicesWeb/customer/src/pages/single/auth/login/language.ts @@ -0,0 +1,22 @@ +export const loginTranslation = { + tr: { + email: "E-Posta", + password: "Şifre", + rememberMe: "Beni Hatırla", + login: "Giriş Yap", + successMessage: "Giriş Yapıldı", + errorMessage: "Giriş Yapılmadı", + pendingMessage: "Giriş Yapılıyor...", + responseData: "Giriş Verileri", + }, + en: { + email: "Email", + password: "Password", + rememberMe: "Remember Me", + login: "Login", + successMessage: "Login completed successfully", + errorMessage: "Login failed", + pendingMessage: "Logging in...", + responseData: "Response Data", + }, +}; diff --git a/ServicesWeb/customer/src/pages/single/auth/login/page.tsx b/ServicesWeb/customer/src/pages/single/auth/login/page.tsx new file mode 100644 index 0000000..f440915 --- /dev/null +++ b/ServicesWeb/customer/src/pages/single/auth/login/page.tsx @@ -0,0 +1,62 @@ +"use client"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { LoginFormData } from "./types"; +import { loginSchemaEmail } from "./schemas"; +import { loginTranslation } from "./language"; +import { loginHook } from "./hook"; +import { AuthPageProps } from "@/validations/mutual/auth/props"; + +function Login({ language }: AuthPageProps) { + const Router = useRouter(); + const translation = loginTranslation[language]; + + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [jsonText, setJsonText] = useState(null); + + const { register, formState: { errors }, handleSubmit, } = useForm({ resolver: zodResolver(loginSchemaEmail) }); + const onSubmit = async (data: LoginFormData) => { loginHook(startTransition, data, setError, setJsonText, Router, language) }; + + return ( + <> +
    +
    +

    {translation.login}

    +
    +
    + + + {errors.email && (

    {errors.email.message}

    )} +
    +
    + + + {errors.password && (

    {errors.password.message}

    )} +
    + {error && (

    {error}

    )} + +
    +
    + {jsonText && ( +
    +

    {translation.responseData}

    +
    + {Object.entries(JSON.parse(jsonText)).map(([key, value]) => ( +
    + {key}: + {typeof value === "object" ? JSON.stringify(value) : value?.toString() || "N/A"} +
    + ))} +
    +
    + )} +
    + + ); +} + +export default Login; diff --git a/ServicesWeb/customer/src/pages/single/auth/login/schemas.ts b/ServicesWeb/customer/src/pages/single/auth/login/schemas.ts new file mode 100644 index 0000000..9b688a4 --- /dev/null +++ b/ServicesWeb/customer/src/pages/single/auth/login/schemas.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +const loginSchemaEmail = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(5, "Password must be at least 5 characters"), + rememberMe: z.boolean().optional().default(false), +}); + +const loginSchemaPhone = z.object({ + phone: z.string().regex(/^[0-9]{10}$/, "Invalid phone number"), + password: z.string().min(5, "Password must be at least 5 characters"), + rememberMe: z.boolean().optional().default(false), +}); + +export { loginSchemaEmail, loginSchemaPhone }; diff --git a/ServicesWeb/customer/src/pages/single/auth/login/types.ts b/ServicesWeb/customer/src/pages/single/auth/login/types.ts new file mode 100644 index 0000000..0ed3a33 --- /dev/null +++ b/ServicesWeb/customer/src/pages/single/auth/login/types.ts @@ -0,0 +1,13 @@ +type LoginFormData = { + email: string; + password: string; + rememberMe?: boolean; +}; + +type LoginFormDataPhone = { + phone: string; + password: string; + rememberMe?: boolean; +}; + +export type { LoginFormData, LoginFormDataPhone }; diff --git a/ServicesWeb/customer/src/validations/mutual/auth/props.ts b/ServicesWeb/customer/src/validations/mutual/auth/props.ts new file mode 100644 index 0000000..2a8169a --- /dev/null +++ b/ServicesWeb/customer/src/validations/mutual/auth/props.ts @@ -0,0 +1,13 @@ +import { LanguageTypes } from "../language/validations"; + +// +interface AuthLayoutProps { + page: React.ReactNode; + lang: LanguageTypes; + activePageUrl: string; +} +interface AuthPageProps { + query?: { [key: string]: string | string[] | undefined }; + language: LanguageTypes; +} +export type { AuthLayoutProps, AuthPageProps }; diff --git a/ServicesWeb/customer/src/validations/mutual/pages/props.ts b/ServicesWeb/customer/src/validations/mutual/pages/props.ts new file mode 100644 index 0000000..7148508 --- /dev/null +++ b/ServicesWeb/customer/src/validations/mutual/pages/props.ts @@ -0,0 +1,7 @@ +type ParamsType = { page: string[] | undefined }; +interface AuthServerProps { + params: Promise<{ page: string[] | undefined }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export type { ParamsType, AuthServerProps }; diff --git a/example_component.html b/example_component.html new file mode 100755 index 0000000..cbcfaf6 --- /dev/null +++ b/example_component.html @@ -0,0 +1,2288 @@ + + + + + + + + + + + + + + + + + + + + + + + Admin Panel + + + + + + + + + +
    + +
    + + + +
    + + + +
    +
    +
    +
    +
    +
    +
    2
    +
    +
    Users
    +
    + +
    + + View +
    +
    +
    +
    +
    +
    100
    +
    +30%
    +
    +
    Companies
    +
    + +
    + View +
    +
    +
    +
    +
    100
    +
    Blogs
    +
    + +
    + View +
    +
    +
    +
    +
    +
    +
    +

    Users

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAmount
    Administrator1 +
    + 70% +
    +
    +
    +
    +
    +
    +
    User6 +
    + 40% +
    +
    +
    +
    +
    +
    +
    User5 +
    + 45% +
    +
    +
    +
    +
    +
    +
    User4 +
    + 60% +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Activities
    + +
    +
    + + + + + + + + + + + + + + + +
    + + + 02-02-2024 + + 17.45 + + +
    + + + 02-02-2024 + + 17.45 + + +
    +
    +
    +
    +
    +
    +
    +
    Order Statistics
    + +
    +
    +
    +
    +
    10
    + $80 +
    + Active +
    +
    +
    +
    50
    + +$469 +
    + Completed +
    +
    +
    +
    4
    + -$130 +
    + Canceled +
    +
    +
    + +
    +
    +
    +
    +
    Earnings
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ServiceEarningStatus
    + + + +$235 + + Pending +
    + + + -$235 + + Withdrawn +
    + + + +$235 + + Pending +
    + + + -$235 + + Withdrawn +
    + + + +$235 + + Pending +
    + + + -$235 + + Withdrawn +
    + + + +$235 + + Pending +
    + + + -$235 + + Withdrawn +
    + + + +$235 + + Pending +
    + + + -$235 + + Withdrawn +
    +
    +
    +
    +
    + +
    + + + + + + + + + + diff --git a/example_components/content/content.html b/example_components/content/content.html new file mode 100644 index 0000000..40549a3 --- /dev/null +++ b/example_components/content/content.html @@ -0,0 +1,188 @@ + +
    +
    +
    +
    +
    +
    +
    2
    +
    +
    Users
    +
    + +
    + View +
    +
    +
    +
    +
    +
    100
    +
    +30%
    +
    +
    Companies
    +
    + +
    + View +
    +
    +
    +
    +
    100
    +
    Blogs
    +
    + +
    + View +
    +
    + + +
    + +
    +
    +
    +
    +

    Users

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    RoleAmount
    Administrator1 +
    + 70% +
    +
    +
    +
    +
    +
    +
    User6 +
    + 40% +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    Activities
    + +
    +
    + + + + + + + + + + + + + +
    + + + 02-02-2024 + + Completed +
    + + + 02-02-2024 + + Cancelled +
    +
    +
    +
    +
    + diff --git a/example_components/index.html b/example_components/index.html new file mode 100644 index 0000000..1d033fd --- /dev/null +++ b/example_components/index.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + Admin Panel + + + + + + + + + +
    + + + + + +
    + +
    + + + + + + + + + + diff --git a/example_components/navbar/navbar.html b/example_components/navbar/navbar.html new file mode 100644 index 0000000..387e083 --- /dev/null +++ b/example_components/navbar/navbar.html @@ -0,0 +1,94 @@ + +
    + + + +
    + diff --git a/example_components/scripts/popper.js b/example_components/scripts/popper.js new file mode 100644 index 0000000..1f740b9 --- /dev/null +++ b/example_components/scripts/popper.js @@ -0,0 +1,88 @@ +// Popper functionality for dropdowns +document.addEventListener('DOMContentLoaded', function() { + // start: Popper + const popperInstance = {}; + document.querySelectorAll('.dropdown').forEach(function (item, index) { + const popperId = 'popper-' + index; + const toggle = item.querySelector('.dropdown-toggle'); + const menu = item.querySelector('.dropdown-menu'); + + if (toggle && menu) { + menu.dataset.popperId = popperId; + popperInstance[popperId] = Popper.createPopper(toggle, menu, { + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 8], + }, + }, + { + name: 'preventOverflow', + options: { + padding: 24, + }, + }, + ], + placement: 'bottom-end' + }); + } + }); + + document.addEventListener('click', function (e) { + const toggle = e.target.closest('.dropdown-toggle'); + const menu = e.target.closest('.dropdown-menu'); + + if (toggle) { + const menuEl = toggle.closest('.dropdown').querySelector('.dropdown-menu'); + const popperId = menuEl.dataset.popperId; + + if (menuEl.classList.contains('hidden')) { + hideDropdown(); + menuEl.classList.remove('hidden'); + showPopper(popperId); + } else { + menuEl.classList.add('hidden'); + hidePopper(popperId); + } + } else if (!menu) { + hideDropdown(); + } + }); + + function hideDropdown() { + document.querySelectorAll('.dropdown-menu').forEach(function (item) { + item.classList.add('hidden'); + }); + } + + function showPopper(popperId) { + if (popperInstance[popperId]) { + popperInstance[popperId].setOptions(function (options) { + return { + ...options, + modifiers: [ + ...options.modifiers, + { name: 'eventListeners', enabled: true }, + ], + } + }); + popperInstance[popperId].update(); + } + } + + function hidePopper(popperId) { + if (popperInstance[popperId]) { + popperInstance[popperId].setOptions(function (options) { + return { + ...options, + modifiers: [ + ...options.modifiers, + { name: 'eventListeners', enabled: false }, + ], + } + }); + } + } + // end: Popper +}); diff --git a/example_components/scripts/sidebar.js b/example_components/scripts/sidebar.js new file mode 100644 index 0000000..aca4017 --- /dev/null +++ b/example_components/scripts/sidebar.js @@ -0,0 +1,42 @@ +// Sidebar functionality +document.addEventListener('DOMContentLoaded', function() { + // start: Sidebar + const sidebarToggle = document.querySelector('.sidebar-toggle'); + const sidebarOverlay = document.querySelector('.sidebar-overlay'); + const sidebarMenu = document.querySelector('.sidebar-menu'); + const main = document.querySelector('.main'); + + if (sidebarToggle) { + sidebarToggle.addEventListener('click', function (e) { + e.preventDefault(); + main.classList.toggle('active'); + sidebarOverlay.classList.toggle('hidden'); + sidebarMenu.classList.toggle('-translate-x-full'); + }); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', function (e) { + e.preventDefault(); + main.classList.add('active'); + sidebarOverlay.classList.add('hidden'); + sidebarMenu.classList.add('-translate-x-full'); + }); + } + + document.querySelectorAll('.sidebar-dropdown-toggle').forEach(function (item) { + item.addEventListener('click', function (e) { + e.preventDefault(); + const parent = item.closest('.group'); + if (parent.classList.contains('selected')) { + parent.classList.remove('selected'); + } else { + document.querySelectorAll('.sidebar-dropdown-toggle').forEach(function (i) { + i.closest('.group').classList.remove('selected'); + }); + parent.classList.add('selected'); + } + }); + }); + // end: Sidebar +}); diff --git a/example_components/scripts/tabs.js b/example_components/scripts/tabs.js new file mode 100644 index 0000000..4a14a18 --- /dev/null +++ b/example_components/scripts/tabs.js @@ -0,0 +1,29 @@ +// Tab functionality for notifications and messages +document.addEventListener('DOMContentLoaded', function() { + // start: Tab + document.querySelectorAll('[data-tab]').forEach(function (item) { + item.addEventListener('click', function (e) { + e.preventDefault(); + const tab = item.dataset.tab; + const page = item.dataset.tabPage; + const target = document.querySelector('[data-tab-for="' + tab + '"][data-page="' + page + '"]'); + + // Remove active class from all tab buttons + document.querySelectorAll('[data-tab="' + tab + '"]').forEach(function (i) { + i.classList.remove('active'); + }); + + // Hide all tab content + document.querySelectorAll('[data-tab-for="' + tab + '"]').forEach(function (i) { + i.classList.add('hidden'); + }); + + // Add active class to clicked tab button and show its content + item.classList.add('active'); + if (target) { + target.classList.remove('hidden'); + } + }); + }); + // end: Tab +}); diff --git a/example_components/sidebar/sidebar.html b/example_components/sidebar/sidebar.html new file mode 100644 index 0000000..fffd9d5 --- /dev/null +++ b/example_components/sidebar/sidebar.html @@ -0,0 +1,75 @@ + + + + diff --git a/example_components/styles/main.css b/example_components/styles/main.css new file mode 100644 index 0000000..2203912 --- /dev/null +++ b/example_components/styles/main.css @@ -0,0 +1,83 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); + +/* Base styles */ +body { + font-family: 'Inter', sans-serif; +} + +/* Sidebar styles */ +.sidebar-menu { + transition: transform 0.3s ease; +} + +.sidebar-menu.-translate-x-full { + transform: translateX(-100%); +} + +/* Dropdown styles */ +.dropdown-menu { + transition: all 0.2s ease; +} + +/* Active states */ +.active { + border-bottom: 2px solid #f84525; + color: #111827; +} + +/* Group selected states */ +.group.selected .group-[\.selected]\:rotate-90 { + transform: rotate(90deg); +} + +.group.selected .group-[\.selected]\:block { + display: block; +} + +.group.selected .group-[\.selected]\:bg-gray-950 { + --tw-bg-opacity: 1; + background-color: rgb(3 7 18 / var(--tw-bg-opacity)); +} + +.group.active .group-[\.active]\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.group.selected .group-[\.selected]\:text-gray-100 { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} + +/* Responsive styles */ +@media (min-width: 768px) { + .md\:ml-64 { + margin-left: 16rem; + } + + .md\:hidden { + display: none; + } + + .md\:w-\[calc\(100\%-256px\)\] { + width: calc(100% - 256px); + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 1024px) { + .lg\:col-span-2 { + grid-column: span 2 / span 2; + } + + .lg\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +}