login setup client added

This commit is contained in:
Berkay 2025-05-13 21:08:09 +03:00
parent 015a84ccf6
commit b15f58c319
368 changed files with 12644 additions and 19 deletions

View File

@ -19,10 +19,10 @@ COPY /api_services/schemas /schemas
COPY /api_services/api_modules /api_modules
COPY /api_services/api_middlewares /api_middlewares
COPY /api_services/api_builds/auth-service/endpoints /api_initializer/endpoints
COPY /api_services/api_builds/auth-service/events /api_initializer/events
COPY /api_services/api_builds/auth-service/validations /api_initializer/validations
COPY /api_services/api_builds/auth-service/index.py /api_initializer/index.py
COPY /api_services/api_builds/auth_service/endpoints /api_initializer/endpoints
COPY /api_services/api_builds/auth_service/events /api_initializer/events
COPY /api_services/api_builds/auth_service/validations /api_initializer/validations
# COPY /api_services/api_builds/auth_service/index.py /api_initializer/index.py
# Set Python path to include app directory
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1

View File

@ -14,7 +14,6 @@ from validations.request.auth.validations import (
RequestForgotPasswordPhone,
RequestForgotPasswordEmail,
RequestVerifyOTP,
RequestApplication,
)
from events.auth.events import AuthHandlers
from endpoints.index import endpoints_index

View File

@ -1,7 +1,16 @@
from typing import Any, Union
from api_validations.token.validations import TokenDictType, EmployeeTokenObject, OccupantTokenObject, CompanyToken, OccupantToken, UserType
from api_validations.token.validations import (
TokenDictType,
EmployeeTokenObject,
OccupantTokenObject,
CompanyToken,
OccupantToken,
UserType
)
from api_controllers.redis.database import RedisActions
from api_modules.token.password_module import PasswordModule
from schemas import Users
@ -24,7 +33,7 @@ class RedisHandlers:
@classmethod
def get_object_from_redis(cls, access_token: str) -> TokenDictType:
redis_response = RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, access_token, "*"])
redis_response = RedisActions.get_json(list_keys=[cls.AUTH_TOKEN, access_token, "*"])
if not redis_response.status:
raise ValueError("EYS_0001")
if redis_object := redis_response.first:
@ -33,21 +42,21 @@ class RedisHandlers:
@classmethod
def set_object_to_redis(cls, user: Users, token, header_info):
result_delete = RedisActions.delete(list_keys=[RedisHandlers.AUTH_TOKEN, "*", str(user.uu_id)])
result_delete = RedisActions.delete(list_keys=[cls.AUTH_TOKEN, "*", str(user.uu_id)])
generated_access_token = PasswordModule.generate_access_token()
keys = [RedisHandlers.AUTH_TOKEN, generated_access_token, str(user.uu_id)]
keys = [cls.AUTH_TOKEN, generated_access_token, str(user.uu_id)]
RedisActions.set_json(list_keys=keys, value={**token, **header_info}, expires={"hours": 1, "minutes": 30})
return generated_access_token
@classmethod
def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]):
if already_token_data := RedisActions.get_json(list_keys=[RedisHandlers.AUTH_TOKEN, token, "*"]).first:
if already_token_data := RedisActions.get_json(list_keys=[cls.AUTH_TOKEN, token, "*"]).first:
already_token = cls.process_redis_object(already_token_data)
if already_token.is_employee and isinstance(add_payload, CompanyToken):
already_token.selected_company = add_payload
elif already_token.is_occupant and isinstance(add_payload, OccupantToken):
already_token.selected_occupant = add_payload
list_keys = [RedisHandlers.AUTH_TOKEN, token, str(already_token.user_uu_id)]
list_keys = [cls.AUTH_TOKEN, token, str(already_token.user_uu_id)]
result = RedisActions.set_json(list_keys=list_keys, value=already_token.model_dump(), expires={"hours": 1, "minutes": 30})
return result.first
raise ValueError("Something went wrong")

View File

@ -1,4 +1,36 @@
services:
client_frontend:
container_name: client_frontend
build:
context: .
dockerfile: web_services/client_frontend/Dockerfile
networks:
- wag-services
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- WEB_BASE_URL=http://localhost:3000
- API_BASE_URL=http://localhost:3000/api
cpus: 1
mem_limit: 2048m
management_frontend:
container_name: management_frontend
build:
context: .
dockerfile: web_services/management_frontend/Dockerfile
networks:
- wag-services
ports:
- "3001:3000"
environment:
- NODE_ENV=development
- WEB_BASE_URL=http://localhost:3000
- API_BASE_URL=http://localhost:3000/api
cpus: 1
mem_limit: 2048m
auth_service:
container_name: auth_service
build:
@ -6,6 +38,8 @@ services:
dockerfile: api_services/api_builds/auth_service/Dockerfile
env_file:
- api_env.env
networks:
- wag-services
environment:
- API_PATH=app:app
- API_HOST=0.0.0.0
@ -28,6 +62,8 @@ services:
dockerfile: api_services/api_builds/restriction_service/Dockerfile
env_file:
- api_env.env
networks:
- wag-services
environment:
- API_PATH=app:app
- API_HOST=0.0.0.0
@ -49,6 +85,8 @@ services:
dockerfile: api_services/api_builds/management_service/Dockerfile
env_file:
- api_env.env
networks:
- wag-services
environment:
- API_PATH=app:app
- API_HOST=0.0.0.0

View File

@ -0,0 +1,21 @@
FROM node:23-alpine
WORKDIR /
# Copy package.json and package-lock.json
COPY /web_services/client_frontend/package*.json ./web_services/client_frontend/
# Install dependencies
RUN cd ./web_services/client_frontend && npm install --legacy-peer-deps
# Copy the rest of the application
COPY /web_services/client_frontend ./web_services/client_frontend
## Build the Next.js app
#RUN cd ./web_services/client_frontend && npm run dev
# Expose the port the app runs on
EXPOSE 3000
# Command to run the app
CMD ["sh", "-c", "cd /web_services/client_frontend && npm run dev"]

View File

@ -0,0 +1,177 @@
"use server";
import { retrieveAccessToken } from "@/apicalls/mutual/cookies/token";
import {
DEFAULT_RESPONSE,
defaultHeaders,
FetchOptions,
HttpMethod,
ApiResponse,
DEFAULT_TIMEOUT,
} from "./basics";
/**
* Creates a promise that rejects after a specified timeout
* @param ms Timeout in milliseconds
* @param controller AbortController to abort the fetch request
* @returns A promise that rejects after the timeout
*/
const createTimeoutPromise = (
ms: number,
controller: AbortController
): Promise<never> => {
return new Promise((_, reject) => {
setTimeout(() => {
controller.abort();
reject(new Error(`Request timed out after ${ms}ms`));
}, ms);
});
};
/**
* Prepares a standardized API response
* @param response The response data
* @param statusCode HTTP status code
* @returns Standardized API response
*/
const prepareResponse = <T>(
response: T,
statusCode: number
): ApiResponse<T> => {
try {
return {
status: statusCode,
data: response || ({} as T),
};
} catch (error) {
console.error("Error preparing response:", error);
return {
...DEFAULT_RESPONSE,
error: "Response parsing error",
} as ApiResponse<T>;
}
};
/**
* Core fetch function with timeout and error handling
* @param url The URL to fetch
* @param options Fetch options
* @param headers Request headers
* @param payload Request payload
* @returns API response
*/
async function coreFetch<T>(
url: string,
options: FetchOptions = {},
headers: Record<string, string> = defaultHeaders,
payload?: any
): Promise<ApiResponse<T>> {
const { method = "POST", cache = false, timeout = DEFAULT_TIMEOUT } = options;
try {
const controller = new AbortController();
const signal = controller.signal;
const fetchOptions: RequestInit = {
method,
headers,
cache: cache ? "force-cache" : "no-cache",
signal,
};
// Add body if needed
if (method !== "GET" && payload) {
fetchOptions.body = JSON.stringify(
// Handle special case for updateDataWithToken
payload.payload ? payload.payload : payload
);
}
// Create timeout promise
const timeoutPromise = createTimeoutPromise(timeout, controller);
// Race between fetch and timeout
const response = (await Promise.race([
fetch(url, fetchOptions),
timeoutPromise,
])) as Response;
const responseJson = await response.json();
if (process.env.NODE_ENV !== "production") {
console.log("Fetching:", url, fetchOptions);
console.log("Response:", responseJson);
}
return prepareResponse(responseJson, response.status);
} catch (error) {
console.error(`Fetch error (${url}):`, error);
return {
...DEFAULT_RESPONSE,
error: error instanceof Error ? error.message : "Network error",
} as ApiResponse<T>;
}
}
/**
* Fetch data without authentication
*/
async function fetchData<T>(
endpoint: string,
payload?: any,
method: HttpMethod = "POST",
cache: boolean = false,
timeout: number = DEFAULT_TIMEOUT
): Promise<ApiResponse<T>> {
return coreFetch<T>(
endpoint,
{ method, cache, timeout },
defaultHeaders,
payload
);
}
/**
* Fetch data with authentication token
*/
async function fetchDataWithToken<T>(
endpoint: string,
payload?: any,
method: HttpMethod = "POST",
cache: boolean = false,
timeout: number = DEFAULT_TIMEOUT
): Promise<ApiResponse<T>> {
const accessToken = (await retrieveAccessToken()) || "";
const headers = {
...defaultHeaders,
"eys-acs-tkn": accessToken,
};
return coreFetch<T>(endpoint, { method, cache, timeout }, headers, payload);
}
/**
* Update data with authentication token and UUID
*/
async function updateDataWithToken<T>(
endpoint: string,
uuid: string,
payload?: any,
method: HttpMethod = "POST",
cache: boolean = false,
timeout: number = DEFAULT_TIMEOUT
): Promise<ApiResponse<T>> {
const accessToken = (await retrieveAccessToken()) || "";
const headers = {
...defaultHeaders,
"eys-acs-tkn": accessToken,
};
return coreFetch<T>(
`${endpoint}/${uuid}`,
{ method, cache, timeout },
headers,
payload
);
}
export { fetchData, fetchDataWithToken, updateDataWithToken };

View File

@ -0,0 +1,67 @@
const formatServiceUrl = (url: string) => {
if (!url) return "";
return url.startsWith("http") ? url : `http://${url}`;
};
const baseUrlAuth = formatServiceUrl(
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
);
const baseUrlPeople = formatServiceUrl(
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002"
);
const baseUrlApplication = formatServiceUrl(
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "management_service:8004"
);
// Types for better type safety
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
interface ApiResponse<T = any> {
status: number;
data: T;
error?: string;
}
interface FetchOptions {
method?: HttpMethod;
cache?: boolean;
timeout?: number;
}
const tokenSecret = process.env.TOKENSECRET_90 || "";
const cookieObject: any = {
httpOnly: true,
path: "/",
sameSite: "none",
secure: true,
maxAge: 3600,
priority: "high",
};
// Constants
const DEFAULT_TIMEOUT = 10000; // 10 seconds
const defaultHeaders = {
accept: "application/json",
language: "tr",
domain: "management.com.tr",
tz: "GMT+3",
"Content-type": "application/json",
};
const DEFAULT_RESPONSE: ApiResponse = {
error: "Hata tipi belirtilmedi",
status: 500,
data: {},
};
export type { HttpMethod, ApiResponse, FetchOptions };
export {
DEFAULT_TIMEOUT,
DEFAULT_RESPONSE,
defaultHeaders,
baseUrlAuth,
baseUrlPeople,
baseUrlApplication,
tokenSecret,
cookieObject,
};

View File

@ -0,0 +1,182 @@
"use server";
import NextCrypto from "next-crypto";
import { fetchData, fetchDataWithToken } from "@/apicalls/api-fetcher";
import { baseUrlAuth, cookieObject, tokenSecret } from "@/apicalls/basics";
import { cookies } from "next/headers";
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`;
const logoutEndpoint = `${baseUrlAuth}/authentication/logout`;
console.log("loginEndpoint", loginEndpoint);
console.log("loginSelectEndpoint", loginSelectEndpoint);
interface LoginViaAccessKeys {
accessKey: string;
password: string;
rememberMe: boolean;
}
interface LoginSelectEmployee {
company_uu_id: string;
}
interface LoginSelectOccupant {
build_living_space_uu_id: any;
}
async function logoutActiveSession() {
const cookieStore = await cookies();
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
cookieStore.delete("accessToken");
cookieStore.delete("accessObject");
cookieStore.delete("userProfile");
cookieStore.delete("userSelection");
return response;
}
async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
try {
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const response = await fetchData(
loginEndpoint,
{
access_key: payload.accessKey,
password: payload.password,
remember_me: payload.rememberMe,
},
"POST",
false
);
console.log("response", response);
if (response.status === 200 || response.status === 202) {
const loginRespone: any = response?.data;
const accessToken = await nextCrypto.encrypt(loginRespone.access_token);
const accessObject = await nextCrypto.encrypt(
JSON.stringify({
userType: loginRespone.user_type,
selectionList: loginRespone.selection_list,
})
);
const userProfile = await nextCrypto.encrypt(
JSON.stringify(loginRespone.user)
);
cookieStore.set({
name: "accessToken",
value: accessToken,
...cookieObject,
});
cookieStore.set({
name: "accessObject",
value: accessObject,
...cookieObject,
});
cookieStore.set({
name: "userProfile",
value: JSON.stringify(userProfile),
...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: LoginSelectEmployee) {
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const companyUUID = payload.company_uu_id;
const selectResponse: any = await fetchDataWithToken(
loginSelectEndpoint,
{
company_uu_id: companyUUID,
},
"POST",
false
);
cookieStore.delete("userSelection");
if (selectResponse.status === 200 || selectResponse.status === 202) {
const usersSelection = await nextCrypto.encrypt(
JSON.stringify({
selected: companyUUID,
user_type: "employee",
})
);
cookieStore.set({
name: "userSelection",
value: usersSelection,
...cookieObject,
});
}
return selectResponse;
}
async function loginSelectOccupant(payload: LoginSelectOccupant) {
const livingSpaceUUID = payload.build_living_space_uu_id;
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const selectResponse: any = await fetchDataWithToken(
loginSelectEndpoint,
{
build_living_space_uu_id: livingSpaceUUID,
},
"POST",
false
);
cookieStore.delete("userSelection");
if (selectResponse.status === 200 || selectResponse.status === 202) {
const usersSelection = await nextCrypto.encrypt(
JSON.stringify({
selected: livingSpaceUUID,
user_type: "occupant",
})
);
cookieStore.set({
name: "userSelection",
value: usersSelection,
...cookieObject,
});
}
return selectResponse;
}
export {
loginViaAccessKeys,
loginSelectEmployee,
loginSelectOccupant,
logoutActiveSession,
};

View File

@ -0,0 +1,148 @@
"use server";
import { fetchDataWithToken } from "@/apicalls/api-fetcher";
import { baseUrlAuth, tokenSecret } from "@/apicalls/basics";
import { cookies } from "next/headers";
import NextCrypto from "next-crypto";
const checkToken = `${baseUrlAuth}/authentication/token/check`;
const pageValid = `${baseUrlAuth}/authentication/page/valid`;
const siteUrls = `${baseUrlAuth}/authentication/sites/list`;
const nextCrypto = new NextCrypto(tokenSecret);
async function checkAccessTokenIsValid() {
const response = await fetchDataWithToken(checkToken, {}, "GET", false);
return response?.status === 200 || response?.status === 202 ? true : false;
}
async function retrievePageList() {
const response: any = await fetchDataWithToken(siteUrls, {}, "GET", false);
return response?.status === 200 || response?.status === 202
? response.data?.sites
: null;
}
async function retrieveApplicationbyUrl(pageUrl: string) {
const response: any = await fetchDataWithToken(
pageValid,
{
page_url: pageUrl,
},
"POST",
false
);
return response?.status === 200 || response?.status === 202
? response.data?.application
: null;
}
async function retrieveAccessToken() {
const cookieStore = await cookies();
const encrpytAccessToken = cookieStore.get("accessToken")?.value || "";
return encrpytAccessToken
? await nextCrypto.decrypt(encrpytAccessToken)
: null;
}
async function retrieveUserType() {
const cookieStore = await cookies();
const encrpytaccessObject = cookieStore.get("accessObject")?.value || "{}";
const decrpytUserType = JSON.parse(
(await nextCrypto.decrypt(encrpytaccessObject)) || "{}"
);
return decrpytUserType ? decrpytUserType : null;
}
async function retrieveAccessObjects() {
const cookieStore = await cookies();
const encrpytAccessObject = cookieStore.get("accessObject")?.value || "";
const decrpytAccessObject = await nextCrypto.decrypt(encrpytAccessObject);
return decrpytAccessObject ? JSON.parse(decrpytAccessObject) : null;
}
async function retrieveUserSelection() {
const cookieStore = await cookies();
const encrpytUserSelection = cookieStore.get("userSelection")?.value || "";
let objectUserSelection = {};
let decrpytUserSelection: any = await nextCrypto.decrypt(
encrpytUserSelection
);
decrpytUserSelection = decrpytUserSelection
? JSON.parse(decrpytUserSelection)
: null;
console.log("decrpytUserSelection", decrpytUserSelection);
const userSelection = decrpytUserSelection?.selected;
const accessObjects = (await retrieveAccessObjects()) || {};
console.log("accessObjects", accessObjects);
if (decrpytUserSelection?.user_type === "employee") {
const companyList = accessObjects?.selectionList;
const selectedCompany = companyList.find(
(company: any) => company.uu_id === userSelection
);
if (selectedCompany) {
objectUserSelection = { userType: "employee", selected: selectedCompany };
}
} else if (decrpytUserSelection?.user_type === "occupant") {
const buildingsList = accessObjects?.selectionList;
// Iterate through all buildings
if (buildingsList) {
// Loop through each building
for (const buildKey in buildingsList) {
const building = buildingsList[buildKey];
// Check if the building has occupants
if (building.occupants && building.occupants.length > 0) {
// Find the occupant with the matching build_living_space_uu_id
const occupant = building.occupants.find(
(occ: any) => occ.build_living_space_uu_id === userSelection
);
if (occupant) {
objectUserSelection = {
userType: "occupant",
selected: {
...occupant,
buildName: building.build_name,
buildNo: building.build_no,
},
};
break;
}
}
}
}
}
return {
...objectUserSelection,
};
}
// const avatarInfo = await retrieveAvatarInfo();
// lang: avatarInfo?.data?.lang
// ? String(avatarInfo?.data?.lang).toLowerCase()
// : undefined,
// avatar: avatarInfo?.data?.avatar,
// fullName: avatarInfo?.data?.full_name,
// async function retrieveAvatarInfo() {
// const response = await fetchDataWithToken(
// `${baseUrlAuth}/authentication/avatar`,
// {},
// "POST"
// );
// return response;
// }
export {
checkAccessTokenIsValid,
retrieveAccessToken,
retrieveUserType,
retrieveAccessObjects,
retrieveUserSelection,
retrieveApplicationbyUrl,
retrievePageList,
// retrieveavailablePages,
};

View File

@ -0,0 +1,6 @@
'use server';
import Login from "@/webPages/auth/Login/page";
const LoginPage = async () => { return <Login language="en" /> };
export default LoginPage;

View File

@ -0,0 +1,19 @@
"use server";
import React from "react";
import Select from "@/webPages/auth/Select/page";
import { redirect } from "next/navigation";
import { checkAccessTokenIsValid, retrieveUserType } from "@/apicalls/mutual/cookies/token";
const SelectPage = async () => {
const token_is_valid = await checkAccessTokenIsValid();
const selection = await retrieveUserType();
const isEmployee = selection?.userType == "employee";
const isOccupant = selection?.userType == "occupant";
const selectionList = selection?.selectionList;
if (!selectionList || !token_is_valid) { redirect("/auth/en/login") }
return <Select selectionList={selectionList} isEmployee={isEmployee} isOccupant={isOccupant} language={"en"} />
}
export default SelectPage;

View File

@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { Suspense } from "react";
export const metadata: Metadata = {
title: "WAG Frontend",
description: "WAG Frontend Application",
};
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
<div className="w-1/4">
<div className="flex flex-col items-center justify-center h-screen bg-purple-600">
<div className="text-2xl font-bold">WAG Frontend</div>
<div className="text-sm text-gray-500 mt-4">
Welcome to the WAG Frontend Application
</div>
</div>
</div>
<div className="w-3/4 text-black">
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</div>
</div>
);
}

View File

@ -0,0 +1,15 @@
'use server';
import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props";
import { DashboardLayout } from "@/layouts/dashboard/layout";
const MainEnPage: React.FC<MaindasboardPageProps> = async ({ params, searchParams }) => {
const parameters = await params;
const searchParameters = await searchParams;
return (
<div className="flex flex-col items-center justify-center">
<DashboardLayout params={parameters} searchParams={searchParameters} lang="en" />
</div>
);
}
export default MainEnPage;

View File

@ -0,0 +1,15 @@
'use server';
import { DashboardLayout } from "@/layouts/dashboard/layout";
import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props";
const MainEnPage: React.FC<MaindasboardPageProps> = async ({ params, searchParams }) => {
const parameters = await params;
const searchParameters = await searchParams;
return (
<div className="flex flex-col items-center justify-center">
<DashboardLayout lang="en" params={parameters} searchParams={searchParameters} />
</div>
);
}
export default MainEnPage;

View File

@ -0,0 +1,15 @@
'use server';
import { DashboardLayout } from "@/layouts/dashboard/layout";
import { MaindasboardPageProps } from "@/validations/mutual/dashboard/props";
const MainTrPage: React.FC<MaindasboardPageProps> = async ({ params, searchParams }) => {
const parameters = await params;
const searchParameters = await searchParams;
return (
<div className="flex flex-col items-center justify-center">
<DashboardLayout lang="tr" params={parameters} searchParams={searchParameters} />
</div>
);
}
export default MainTrPage;

View File

@ -0,0 +1,20 @@
import { retrieveUserSelection } from "@/apicalls/cookies/token";
import { NextResponse } from "next/server";
export async function POST(): Promise<NextResponse> {
try {
const userSelection = await retrieveUserSelection();
console.log("userSelection", userSelection);
if (userSelection) {
return NextResponse.json({
status: 200,
message: "User selection found",
data: userSelection,
});
}
} catch (error) {}
return NextResponse.json({
status: 500,
message: "User selection not found",
});
}

View File

@ -0,0 +1,39 @@
import { loginViaAccessKeys } from "@/apicalls/custom/login/login";
import { NextResponse } from "next/server";
import { loginSchemaEmail } from "@/webPages/auth/Login/schemas";
export async function POST(req: Request): Promise<NextResponse> {
try {
const headers = req.headers;
console.log("headers", Object.entries(headers));
const body = await req.json();
const dataValidated = {
accessKey: body.email,
password: body.password,
rememberMe: body.rememberMe,
};
const validatedLoginBody = loginSchemaEmail.safeParse(body);
if (!validatedLoginBody.success) {
return NextResponse.json({
status: 422,
message: validatedLoginBody.error.message,
});
}
const userLogin = await loginViaAccessKeys(dataValidated);
if (userLogin.status === 200 || userLogin.status === 202) {
return NextResponse.json({
status: 200,
message: "Login successfully completed",
data: userLogin.data,
});
} else {
return NextResponse.json({
status: userLogin.status,
message: userLogin.message,
});
}
} catch (error) {
return NextResponse.json({ status: 401, message: "Invalid credentials" });
}
}

View File

@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
export async function POST() {
async function retrieveAvailableApplication(): Promise<string[]> {
return new Promise((resolve) => {
const mockList = [
"management/account/tenant/something",
"management/account/tenant/somethingSecond",
"building/parts/tenant/something",
];
resolve(mockList);
});
}
const availableApplications = await retrieveAvailableApplication();
return NextResponse.json({
status: 200,
data: availableApplications,
});
}

View File

@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
export async function POST(): Promise<NextResponse> {
async function retrievePageToRender(): Promise<string> {
return new Promise((resolve) => {
resolve("superUserTenantSomething");
});
}
const pageToRender = await retrievePageToRender();
return NextResponse.json({
status: 200,
data: pageToRender,
});
}

View File

@ -0,0 +1,41 @@
import { z } from "zod";
import { loginSelectEmployee } from "@/apicalls/custom/login/login";
import { NextResponse } from "next/server";
const loginSchemaEmployee = z.object({
company_uu_id: z.string(),
});
export async function POST(req: Request): Promise<NextResponse> {
try {
const headers = req.headers;
console.log("headers", Object.entries(headers));
const body = await req.json();
const dataValidated = {
company_uu_id: body.company_uu_id,
};
const validatedLoginBody = loginSchemaEmployee.safeParse(body);
if (!validatedLoginBody.success) {
return NextResponse.json({
status: 422,
message: validatedLoginBody.error.message,
});
}
const userLogin = await loginSelectEmployee(dataValidated);
if (userLogin.status === 200 || userLogin.status === 202) {
return NextResponse.json({
status: 200,
message: "Selection successfully completed",
data: userLogin.data,
});
} else {
return NextResponse.json({
status: userLogin.status,
message: userLogin.message,
});
}
} catch (error) {
return NextResponse.json({ status: 401, message: "Invalid credentials" });
}
}

View File

@ -0,0 +1,41 @@
import { z } from "zod";
import { loginSelectOccupant } from "@/apicalls/login/login";
import { NextResponse } from "next/server";
const loginSchemaOccupant = z.object({
build_living_space_uu_id: z.string(),
});
export async function POST(req: Request): Promise<NextResponse> {
try {
const headers = req.headers;
console.log("headers", Object.entries(headers));
const body = await req.json();
const dataValidated = {
build_living_space_uu_id: body.build_living_space_uu_id,
};
const validatedLoginBody = loginSchemaOccupant.safeParse(body);
if (!validatedLoginBody.success) {
return NextResponse.json({
status: 422,
message: validatedLoginBody.error.message,
});
}
const userLogin = await loginSelectOccupant(dataValidated);
if (userLogin.status === 200 || userLogin.status === 202) {
return NextResponse.json({
status: 200,
message: "Selection successfully completed",
data: userLogin.data,
});
} else {
return NextResponse.json({
status: userLogin.status,
message: userLogin.message,
});
}
} catch (error) {
return NextResponse.json({ status: 401, message: "Invalid credentials" });
}
}

View File

@ -0,0 +1,138 @@
import { NextResponse, NextRequest } from "next/server";
interface APiData {
uuid: string;
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
country: string;
description: string;
isDeleted: boolean;
isConfirmed: boolean;
createdAt: Date;
updatedAt: Date;
}
function generateMockData(volume: number): APiData[] {
const data: APiData[] = [];
for (let i = 0; i < volume; i++) {
data.push({
uuid: i.toString(),
firstName: "test-" + i,
lastName: "test-" + i,
email: "test-" + i,
phoneNumber: "test-" + i,
country: "test-" + i,
description: "test-" + i,
isDeleted: false,
isConfirmed: false,
createdAt: new Date(),
updatedAt: new Date(),
});
}
return data;
}
const apiMockData: APiData[] = generateMockData(10);
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string[] }> }
) {
const id = (await params).id[0];
const data = apiMockData.find((item) => item.uuid === id);
if (!data) {
return NextResponse.json({
status: 404,
data: {
message: "Not Found",
},
});
}
return NextResponse.json({
status: 200,
data: data,
});
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string[] }> }
) {
const id = (await params).id[0];
const body = await request.json();
const idFound = apiMockData.find((item) => item.uuid === id);
if (!idFound) {
return NextResponse.json({
status: 404,
data: {
message: "Not Found",
},
});
}
apiMockData.splice(apiMockData.indexOf(idFound as any), 1);
apiMockData.push({
...idFound,
firstName: body.name || idFound.firstName,
description: body.description || idFound.description,
uuid: id,
isDeleted: false,
createdAt: new Date(),
updatedAt: new Date(),
});
return NextResponse.json({
status: 200,
data: {
...idFound,
firstName: body.name,
description: body.description,
},
});
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string[] }> }
) {
const id = (await params).id[0];
const data = apiMockData.find((item) => item.uuid === id);
if (!data) {
return NextResponse.json({
status: 404,
data: {
message: "Not Found",
},
});
}
apiMockData.splice(apiMockData.indexOf(data as any), 1);
return NextResponse.json({
status: 200,
data: apiMockData.length,
});
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string[] }> }
) {
const id = (await params).id[0];
const body = await request.json();
const data = apiMockData.find((item) => item.uuid === id);
if (!data) {
return NextResponse.json({
status: 404,
data: {
message: "Not Found",
},
});
}
return NextResponse.json({
status: 200,
data: {
...data,
firstName: body.name || data.firstName,
},
});
}

View File

@ -0,0 +1,139 @@
import { NextResponse, NextRequest } from "next/server";
import { randomUUID } from "crypto";
interface APiData {
"Users.uuid": string;
"Users.firstName": string;
"Users.lastName": string;
"Users.email": string;
"Users.phoneNumber": string;
"Users.country": string;
"Users.description": string;
"Users.isDeleted": boolean;
"Users.isConfirmed": boolean;
"Users.createdAt": Date;
"Users.updatedAt": Date;
}
function generateMockData(volume: number): APiData[] {
const data: APiData[] = [];
for (let i = 0; i < volume; i++) {
data.push({
"Users.uuid": randomUUID(),
"Users.firstName": "test-name-" + i,
"Users.lastName": "test-lastName-" + i,
"Users.email": "test-email-" + i,
"Users.phoneNumber": "test-phoneNumber-" + i,
"Users.country": "test-country-" + i,
"Users.description": "test-description-" + i,
"Users.isDeleted": Math.random() > 0.5,
"Users.isConfirmed": Math.random() > 0.5,
"Users.createdAt": new Date(),
"Users.updatedAt": new Date(),
});
}
return data;
}
interface RequestParams {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
const apiMockData: APiData[] = generateMockData(108);
interface PaginationRequest {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
interface PaginationResponse {
onPage: number;
onPageCount: number;
totalPage: number;
totalCount: number;
next: boolean;
back: boolean;
}
interface DataResponse<T> {
data: T[];
pagination: PaginationResponse;
}
interface NextApiResponse<T> {
status: number;
data: DataResponse<T>;
}
export async function POST(
request: NextRequest
): Promise<NextResponse<NextApiResponse<APiData>>> {
const pagination: PaginationRequest = await request.json();
const ceilLength = Math.ceil(apiMockData.length / pagination.size);
const isNext = pagination.page < ceilLength;
const isBack = pagination.page > 1;
const sliceIfPaginationCorrect =
pagination.page <= ceilLength ? pagination.page : ceilLength;
const sliceParams = [
(pagination.page - 1) * pagination.size,
sliceIfPaginationCorrect * pagination.size,
];
const orderField = pagination.orderField;
const orderType = pagination.orderType;
const query = pagination.query;
const filteredData = apiMockData.filter((item) => {
return Object.keys(query).every((key) => {
return item[key as keyof APiData] === query[key];
});
});
if (orderField && orderType) {
for (let i = 0; i < orderField.length; i++) {
const field = orderField[i];
const order = orderType[i];
if (order === "asc") {
filteredData.sort((a, b) => {
if (a[field as keyof APiData] < b[field as keyof APiData]) {
return -1;
}
if (a[field as keyof APiData] > b[field as keyof APiData]) {
return 1;
}
return 0;
});
} else {
filteredData.sort((a, b) => {
if (a[field as keyof APiData] < b[field as keyof APiData]) {
return 1;
}
if (a[field as keyof APiData] > b[field as keyof APiData]) {
return -1;
}
return 0;
});
}
}
}
return NextResponse.json({
status: 200,
data: {
data: filteredData.slice(...sliceParams),
pagination: {
onPage: pagination.page,
onPageCount: pagination.size,
totalPage: ceilLength,
totalCount: apiMockData.length,
next: isNext,
back: isBack,
},
},
});
}

View File

@ -0,0 +1,169 @@
import { NextRequest } from "next/server";
import {
successResponse,
errorResponse,
paginationResponse,
createResponse,
updateResponse,
deleteResponse,
} from "./responseHandlers";
import { withErrorHandling, validateRequiredFields } from "./requestHandlers";
import {
ApiHandler,
PaginationParams,
ListFunction,
CreateFunction,
UpdateFunction,
DeleteFunction,
} from "./types";
/**
* Generic list operation handler
* @param request NextRequest object
* @param body Request body
* @param listFunction The function to call to get the list data
*/
export async function handleListOperation(
request: NextRequest,
body: any,
listFunction: ListFunction
) {
const page = body.page || 1;
const size = body.size || 10;
const orderField = body.orderField || ["uu_id"];
const orderType = body.orderType || ["asc"];
const query = body.query || {};
const response = await listFunction({
page,
size,
orderField,
orderType,
query,
} as PaginationParams);
return paginationResponse(response.data, response.pagination);
}
/**
* Generic create operation handler
* @param request NextRequest object
* @param body Request body
* @param createFunction The function to call to create the item
* @param requiredFields Array of required field names
*/
export async function handleCreateOperation(
body: any,
createFunction?: CreateFunction,
requiredFields: string[] = []
) {
if (requiredFields.length > 0) {
const validation = validateRequiredFields(body, requiredFields);
if (!validation.valid) {
return errorResponse(validation.error as string, 400);
}
}
if (createFunction) {
console.log("Body:", body);
const result = await createFunction(body);
return createResponse(result);
}
return createResponse({
uuid: Math.floor(Math.random() * 1000),
...body,
});
}
/**
* Generic update operation handler
* @param request NextRequest object
* @param body Request body
* @param updateFunction The function to call to update the item
*/
export async function handleUpdateOperation(
request: NextRequest,
body: any,
updateFunction?: UpdateFunction
) {
const uuid = request.nextUrl.searchParams.get("uuid");
if (!uuid) {
return errorResponse("UUID not found", 400);
}
if (updateFunction) {
console.log("Body:", body);
const result = await updateFunction(body, uuid);
return updateResponse(result);
}
return updateResponse(body);
}
/**
* Generic delete operation handler
* @param request NextRequest object
* @param deleteFunction The function to call to delete the item
*/
export async function handleDeleteOperation(
request: NextRequest,
deleteFunction?: DeleteFunction
) {
const uuid = request.nextUrl.searchParams.get("uuid");
if (!uuid) {
return errorResponse("UUID not found", 400);
}
if (deleteFunction) {
await deleteFunction(uuid);
}
return deleteResponse();
}
/**
* Create a wrapped list handler with error handling
* @param listFunction The function to call to get the list data
*/
export function createListHandler(listFunction: ListFunction) {
return withErrorHandling((request: NextRequest, body: any) =>
handleListOperation(request, body, listFunction)
);
}
/**
* Create a wrapped create handler with error handling
* @param createFunction The function to call to create the item
* @param requiredFields Array of required field names
*/
export function createCreateHandler(
createFunction?: CreateFunction,
requiredFields: string[] = []
) {
console.log("Required fields:", requiredFields);
// This handler only takes the body parameter, not the request
return withErrorHandling((body: any) => {
// Ensure we're only passing the actual body data to the create function
if (body && typeof body === 'object' && body.body) {
console.log("Extracting body from request body");
return handleCreateOperation(body.body, createFunction, requiredFields);
}
return handleCreateOperation(body, createFunction, requiredFields);
});
}
/**
* Create a wrapped update handler with error handling
* @param updateFunction The function to call to update the item
*/
export function createUpdateHandler(updateFunction?: UpdateFunction) {
return withErrorHandling((request: NextRequest, body: any) =>
handleUpdateOperation(request, body, updateFunction)
);
}
/**
* Create a wrapped delete handler with error handling
* @param deleteFunction The function to call to delete the item
*/
export function createDeleteHandler(deleteFunction?: DeleteFunction) {
return withErrorHandling((request: NextRequest) =>
handleDeleteOperation(request, deleteFunction)
);
}

View File

@ -0,0 +1,4 @@
// Export all utility functions from a single entry point
export * from './responseHandlers';
export * from './requestHandlers';
export * from './apiOperations';

View File

@ -0,0 +1,70 @@
import { NextRequest } from "next/server";
import { errorResponse } from "./responseHandlers";
import { ValidationResult, ApiHandler, ApiHandlerBodyOnly, ApiHandlerWithRequest } from "./types";
/**
* Safely parse JSON request body with error handling
* @param request NextRequest object
* @returns Parsed request body or null if parsing fails
*/
export async function parseRequestBody(request: NextRequest) {
try {
return await request.json();
} catch (error) {
return null;
}
}
/**
* Wrapper for API route handlers with built-in error handling
* @param handler The handler function to wrap
*/
export function withErrorHandling(
handler: ApiHandler
) {
return async (request: NextRequest) => {
try {
const body = await parseRequestBody(request);
if (body === null) {
return errorResponse("Invalid request body", 400);
}
// Check handler parameter count to determine if it needs request object
// If handler has only 1 parameter, it's likely a create operation that only needs body
if (handler.length === 1) {
// Cast to the appropriate handler type
return await (handler as ApiHandlerBodyOnly)(body);
} else {
// Otherwise pass both request and body (for list, update, delete operations)
return await (handler as ApiHandlerWithRequest)(request, body);
}
} catch (error: any) {
return errorResponse(
error.message || "Internal Server Error",
error.status || 500
);
}
};
}
/**
* Validate that required fields are present in the request body
* @param body Request body
* @param requiredFields Array of required field names
* @returns Object with validation result and error message if validation fails
*/
export function validateRequiredFields(body: any, requiredFields: string[]): ValidationResult {
const missingFields = requiredFields.filter(field =>
body[field] === undefined || body[field] === null || body[field] === ''
);
if (missingFields.length > 0) {
return {
valid: false,
error: `Missing required fields: ${missingFields.join(', ')}`
};
}
return { valid: true };
}

View File

@ -0,0 +1,91 @@
import { NextResponse } from "next/server";
import { ApiResponse, PaginationResponse, PaginatedApiResponse } from "./types";
/**
* Standard success response handler
* @param data The data to return in the response
* @param status HTTP status code (default: 200)
*/
export function successResponse<T>(data: T, status: number = 200) {
return NextResponse.json(
{
success: true,
data,
} as ApiResponse<T>,
{ status }
);
}
/**
* Standard error response handler
* @param message Error message
* @param status HTTP status code (default: 500)
*/
export function errorResponse(message: string, status: number = 500) {
console.error(`API error: ${message}`);
return NextResponse.json(
{
success: false,
error: message
} as ApiResponse<never>,
{ status }
);
}
/**
* Standard pagination response format
* @param data Array of items to return
* @param pagination Pagination information
*/
export function paginationResponse<T>(data: T[], pagination: PaginationResponse | null) {
return NextResponse.json({
data: data || [],
pagination: pagination || {
page: 1,
size: 10,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: ["name"],
orderType: ["asc"],
query: {},
next: false,
back: false,
},
} as PaginatedApiResponse<T>);
}
/**
* Create response handler
* @param data The created item data
*/
export function createResponse<T>(data: T) {
return successResponse(
{
...data as any,
createdAt: new Date().toISOString(),
} as T,
201
);
}
/**
* Update response handler
* @param data The updated item data
*/
export function updateResponse<T>(data: T) {
return successResponse(
{
...data as any,
updatedAt: new Date().toISOString(),
} as T
);
}
/**
* Delete response handler
*/
export function deleteResponse() {
return successResponse({ message: "Item deleted successfully" }, 204);
}

View File

@ -0,0 +1,119 @@
/**
* Type definitions for API utilities
*/
import { NextRequest } from "next/server";
/**
* Validation result interface
*/
export interface ValidationResult {
valid: boolean;
error?: string;
}
/**
* Pagination parameters interface
*/
export interface PaginationParams {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
/**
* Pagination response interface
*/
export interface PaginationResponse {
page: number;
size: number;
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
next: boolean;
back: boolean;
}
export const defaultPaginationResponse: PaginationResponse = {
page: 1,
size: 10,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: ["uu_id"],
orderType: ["asc"],
query: {},
next: false,
back: false,
};
/**
* API response interface
*/
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
/**
* Paginated API response interface
*/
export interface PaginatedApiResponse<T> {
data: T[];
pagination: PaginationResponse;
}
export const collectPaginationFromApiResponse = (
response: PaginatedApiResponse<any>
): PaginationResponse => {
return {
page: response.pagination?.page || 1,
size: response.pagination?.size || 10,
totalCount: response.pagination?.totalCount || 0,
totalItems: response.pagination?.totalItems || 0,
totalPages: response.pagination?.totalPages || 0,
pageCount: response.pagination?.pageCount || 0,
orderField: response.pagination?.orderField || ["uu_id"],
orderType: response.pagination?.orderType || ["asc"],
query: response.pagination?.query || {},
next: response.pagination?.next || false,
back: response.pagination?.back || false,
};
};
/**
* API handler function types
*/
export type ApiHandlerWithRequest = (request: NextRequest, body: any) => Promise<Response>;
export type ApiHandlerBodyOnly = (body: any) => Promise<Response>;
export type ApiHandler = ApiHandlerWithRequest | ApiHandlerBodyOnly;
/**
* List function type
*/
export type ListFunction = (
params: PaginationParams
) => Promise<PaginatedApiResponse<any>>;
/**
* Create function type
*/
export type CreateFunction = (data: any) => Promise<any>;
/**
* Update function type
*/
export type UpdateFunction = (id: any, data: any) => Promise<any>;
/**
* Delete function type
*/
export type DeleteFunction = (id: any) => Promise<any>;

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,11 @@
import { ContentProps } from "@/validations/mutual/dashboard/props";
import { resolveWhichPageToRenderMulti } from "@/pages/resolver/resolver";
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
const PageToBeChildrendMulti: React.FC<ContentProps> = async ({ lang, translations, activePageUrl, mode }) => {
const ApplicationToRender = await resolveWhichPageToRenderMulti({ activePageUrl })
if (!ApplicationToRender) return <ContentToRenderNoPage lang={lang} />
return <ApplicationToRender lang={lang} translations={translations} activePageUrl={activePageUrl} mode={mode} />
}
export default PageToBeChildrendMulti

View File

@ -0,0 +1,13 @@
import { ContentProps } from "@/validations/mutual/dashboard/props";
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
import { resolveWhichPageToRenderSingle } from "@/pages/resolver/resolver";
const PageToBeChildrendSingle: React.FC<ContentProps> = ({ lang, translations, activePageUrl }) => {
const ApplicationToRender = resolveWhichPageToRenderSingle({ activePageUrl })
if (ApplicationToRender) {
return <ApplicationToRender lang={lang} translations={translations} activePageUrl={activePageUrl} />
}
else { return <ContentToRenderNoPage lang={lang} /> }
}
export default PageToBeChildrendSingle

View File

@ -0,0 +1,19 @@
'use server';
import { FC, Suspense } from "react";
import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props";
import LoadingContent from "@/components/mutual/loader/component";
import PageToBeChildrendSingle from "./PageToBeChildrendSingle";
import PageToBeChildrendMulti from "./PageToBeChildrendMulti";
const ContentComponent: FC<ContentProps> = async ({ lang, translations, activePageUrl, isMulti, mode }) => {
const modeFromQuery = ModeTypesList.includes(mode || '') ? mode : 'list'
const renderProps = { lang, translations, activePageUrl, mode: modeFromQuery as ModeTypes }
const PageToBeChildrend = isMulti ? PageToBeChildrendMulti : PageToBeChildrendSingle
const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />
const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]"
return (
<div className={classNameDiv}><Suspense fallback={loadingContent}><PageToBeChildrend {...renderProps} /></Suspense></div>
);
};
export default ContentComponent;

View File

@ -0,0 +1,14 @@
'use server';
import { FC } from "react";
import { langGetKey } from "@/lib/langGet";
import { FooterProps } from "@/validations/mutual/dashboard/props";
const FooterComponent: FC<FooterProps> = ({ translations }) => {
return (
<div className="fixed text-center bottom-0 left-0 right-0 h-16 p-4 border-t border-emerald-150 border-t-2 shadow-sm backdrop-blur-sm bg-emerald-50">
<h1>{langGetKey(translations, "footer")}: {langGetKey(translations, "page")}</h1>
</div>
);
};
export default FooterComponent;

View File

@ -0,0 +1,20 @@
'use server';
import { FC } from "react";
import { HeaderProps } from "@/validations/mutual/dashboard/props";
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
import { langGetKey } from "@/lib/langGet";
const HeaderComponent: FC<HeaderProps> = ({ translations, lang, activePageUrl }) => {
return (
<div className="flex justify-between h-24 items-center p-4 border-emerald-150 border-b-2 shadow-sm backdrop-blur-sm sticky top-0 z-50 bg-emerald-50">
<div className="flex flex-row justify-center items-center">
<p className="text-2xl font-bold mx-3">{langGetKey(translations, 'selectedPage')} :</p>
<p className="text-lg font-bold mx-3"> {langGetKey(translations, 'page')}</p>
</div>
<LanguageSelectionComponent lang={lang} activePage={activePageUrl} />
</div>
);
};
export default HeaderComponent;

View File

@ -0,0 +1,172 @@
'use client';
import { FC, useState, useEffect } from "react";
import { MenuProps } from "@/validations/mutual/dashboard/props";
import { langGetKey } from "@/lib/langGet";
import { parseURlFormString } from "@/lib/menuGet";
import FirstLayerDropdown from "./firstLayerComponent";
import SecondLayerDropdown from "./secondLayerComponent";
import ThirdLayerDropdown from "./thirdLayerComponent";
// Define types for menu structure
type ThirdLayerItem = Record<string, any>;
type SecondLayerItems = Record<string, ThirdLayerItem>;
type FirstLayerItems = Record<string, SecondLayerItems>;
type MenuStructure = FirstLayerItems;
const MenuComponent: FC<MenuProps> = ({ lang, menuItems, menuTranslationsFlatten, activePageUrl }) => {
// State for tracking expanded menu items
const [expandedFirstLayer, setExpandedFirstLayer] = useState<string | null>(null);
const [expandedSecondLayer, setExpandedSecondLayer] = useState<string | null>(null);
const [menuStructure, setMenuStructure] = useState<MenuStructure>({});
// Parse active URL to determine which menu items should be active
const activePathLayers = parseURlFormString(activePageUrl).data;
const activeFirstLayer = activePathLayers[0] || null;
const activeSecondLayer = activePathLayers[1] || null;
const activeThirdLayer = activePathLayers[2] || null;
// Initialize expanded state based on active path
useEffect(() => {
if (activeFirstLayer) {
setExpandedFirstLayer(activeFirstLayer);
if (activeSecondLayer) {
setExpandedSecondLayer(activeSecondLayer);
}
}
}, [activeFirstLayer, activeSecondLayer]);
// Process menu items into a hierarchical structure
useEffect(() => {
const processedStructure: MenuStructure = {};
Object.entries(menuItems).forEach(([path, _]: [string, any]) => {
const layers = parseURlFormString(path).data;
const firstLayer = layers[0];
const secondLayer = layers[1];
const thirdLayer = layers[2];
// Create first layer if it doesn't exist
if (!processedStructure[firstLayer]) {
processedStructure[firstLayer] = {};
}
// Create second layer if it doesn't exist
if (!processedStructure[firstLayer][secondLayer]) {
processedStructure[firstLayer][secondLayer] = {};
}
// Add third layer
processedStructure[firstLayer][secondLayer][thirdLayer] = {};
});
setMenuStructure(processedStructure);
}, [menuItems]);
// Handle click on first layer menu item
const handleFirstLayerClick = (key: string) => {
if (expandedFirstLayer === key) {
// If already expanded, collapse it
setExpandedFirstLayer(null);
setExpandedSecondLayer(null);
} else {
// Otherwise expand it
setExpandedFirstLayer(key);
setExpandedSecondLayer(null);
}
};
// Handle click on second layer menu item
const handleSecondLayerClick = (key: string) => {
if (expandedSecondLayer === key) {
// If already expanded, collapse it
setExpandedSecondLayer(null);
} else {
// Otherwise expand it
setExpandedSecondLayer(key);
}
};
// Render third layer menu items
const renderThirdLayerItems = (firstLayerKey: string, secondLayerKey: string, thirdLayerItems: ThirdLayerItem) => {
const baseUrl = `/${firstLayerKey}/${secondLayerKey}`;
return Object.keys(thirdLayerItems).map(thirdLayerKey => {
const isActive =
activeFirstLayer === firstLayerKey &&
activeSecondLayer === secondLayerKey &&
activeThirdLayer === thirdLayerKey;
const url = `/${lang}${baseUrl}/${thirdLayerKey}`;
return (
<div key={`${thirdLayerKey}-item`} className="ml-2 my-1">
<ThirdLayerDropdown
isActive={isActive}
innerText={langGetKey(menuTranslationsFlatten, thirdLayerKey)}
url={url}
/>
</div>
);
});
};
// Render second layer menu items
const renderSecondLayerItems = (firstLayerKey: string, secondLayerItems: SecondLayerItems) => {
return Object.entries(secondLayerItems).map(([secondLayerKey, thirdLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey;
const isExpanded = expandedSecondLayer === secondLayerKey;
return (
<div key={`${secondLayerKey}-item`} className="ml-2 my-1">
<SecondLayerDropdown
isActive={isActive}
isExpanded={isExpanded}
innerText={langGetKey(menuTranslationsFlatten, secondLayerKey)}
onClick={() => handleSecondLayerClick(secondLayerKey)}
/>
{isExpanded && (
<div className="ml-2 mt-1">
{renderThirdLayerItems(firstLayerKey, secondLayerKey, thirdLayerItems)}
</div>
)}
</div>
);
});
};
// Render first layer menu items
const renderFirstLayerItems = () => {
return Object.entries(menuStructure).map(([firstLayerKey, secondLayerItems]) => {
const isActive = activeFirstLayer === firstLayerKey;
const isExpanded = expandedFirstLayer === firstLayerKey;
return (
<div key={`${firstLayerKey}-item`} className="mb-2">
<FirstLayerDropdown
isActive={isActive}
isExpanded={isExpanded}
innerText={langGetKey(menuTranslationsFlatten, firstLayerKey)}
onClick={() => handleFirstLayerClick(firstLayerKey)}
/>
{isExpanded && (
<div className="mt-1">
{renderSecondLayerItems(firstLayerKey, secondLayerItems)}
</div>
)}
</div>
);
});
};
return (
<div className="fixed top-24 p-5 left-0 right-0 w-80 border-emerald-150 border-r-2 overflow-y-auto h-[calc(100vh-6rem)]">
<div className="flex flex-col">
{renderFirstLayerItems()}
</div>
</div>
);
};
export default MenuComponent;

View File

@ -0,0 +1,41 @@
'use client';
import { FC } from "react";
interface FirstLayerDropdownProps {
isActive: boolean;
isExpanded: boolean;
innerText: string;
onClick: () => void;
}
const FirstLayerDropdown: FC<FirstLayerDropdownProps> = ({ isActive, isExpanded, innerText, onClick }) => {
// Base styles
const baseClassName = "py-3 px-4 text-sm rounded-xl cursor-pointer transition-colors duration-200 flex justify-between items-center w-full";
// Determine the appropriate class based on active and expanded states
let className = baseClassName;
if (isActive) {
className += " bg-emerald-700 text-white font-medium";
} else if (isExpanded) {
className += " bg-emerald-600 text-white";
} else {
className += " bg-emerald-800 text-white hover:bg-emerald-700";
}
return (
<div onClick={onClick} className={className}>
<span>{innerText}</span>
{isExpanded ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)}
</div>
);
};
export default FirstLayerDropdown;

View File

@ -0,0 +1,41 @@
'use client';
import { FC } from "react";
interface SecondLayerDropdownProps {
isActive: boolean;
isExpanded: boolean;
innerText: string;
onClick: () => void;
}
const SecondLayerDropdown: FC<SecondLayerDropdownProps> = ({ isActive, isExpanded, innerText, onClick }) => {
// Base styles
const baseClassName = "py-2 my-1 px-3 text-sm rounded-lg cursor-pointer transition-colors duration-200 flex justify-between items-center w-full";
// Determine the appropriate class based on active and expanded states
let className = baseClassName;
if (isActive) {
className += " bg-emerald-600 text-white font-medium";
} else if (isExpanded) {
className += " bg-emerald-500 text-white";
} else {
className += " bg-emerald-700 text-white hover:bg-emerald-600";
}
return (
<div onClick={onClick} className={className}>
<span>{innerText}</span>
{isExpanded ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)}
</div>
);
};
export default SecondLayerDropdown;

View File

@ -0,0 +1,50 @@
'use client'
import { FC, useState } from "react";
import Link from "next/link";
import LoadingContent from "@/components/mutual/loader/component";
interface ThirdLayerDropdownProps {
isActive: boolean,
innerText: string,
url: string,
}
const ThirdLayerDropdown: FC<ThirdLayerDropdownProps> = ({ isActive, innerText, url }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
// Base styles
const baseClassName = "py-2 my-1 px-3 text-sm rounded-lg bg-black transition-colors duration-200 flex items-center w-full";
// Determine the appropriate class based on active state
let className = baseClassName;
if (isActive) {
className += " bg-emerald-500 text-white font-medium";
} else {
className += " bg-emerald-600 text-white hover:bg-emerald-500";
}
if (isActive) {
return (
<div className={`${className} cursor-not-allowed`}>
<span className="ml-2">{innerText}</span>
</div>
);
} else if (isLoading) {
return (
<div className={className}>
<LoadingContent height="h-5" size="w-5 h-5" plane="" />
<span className="ml-2">{innerText}</span>
</div>
);
} else {
return (
<Link href={url} onClick={() => setIsLoading(true)} className="block">
<div className={className}>
<span className="ml-2">{innerText}</span>
</div>
</Link>
);
}
};
export default ThirdLayerDropdown;

View File

@ -0,0 +1,5 @@
interface IntrerfaceLayerDropdown {
isActive: boolean;
innerText: string;
onClick: () => void;
}

View File

@ -0,0 +1,40 @@
'use server';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from "@/components/mutual/shadcnui/dropdown-menu";
import { Button } from "@/components/mutual/shadcnui/button";
import { languageSelectionTranslation } from "@/languages/mutual/languageSelection";
import { langGetKey, langGet } from "@/lib/langGet";
import { LanguageTypes } from "@/validations/mutual/language/validations";
import LanguageSelectionItem from "./languageItem";
const LanguageSelectionComponent: React.FC<{ lang: LanguageTypes, activePage: string }> = ({ lang, activePage }) => {
const translations = langGet(lang, languageSelectionTranslation);
const getPageWithLocale = (locale: LanguageTypes): string => { return `/${locale}/${activePage}` }
const englishButtonProps = {
activeLang: lang,
buttonsLang: "en",
refUrl: getPageWithLocale("en"),
innerText: langGetKey(translations, "english")
}
const turkishButtonProps = {
activeLang: lang,
buttonsLang: "tr",
refUrl: getPageWithLocale("tr"),
innerText: langGetKey(translations, "turkish")
}
return (
<div className="flex items-end justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="w-48 h-12 text-center text-md">{langGetKey(translations, "title")}</Button>
</DropdownMenuTrigger>
<LanguageSelectionItem {...englishButtonProps} />
<LanguageSelectionItem {...turkishButtonProps} />
</DropdownMenu>
</div>
);
};
export default LanguageSelectionComponent;

View File

@ -0,0 +1,38 @@
'use client';
import { useState, FC } from "react";
import { DropdownMenuContent, DropdownMenuLabel } from "@/components/mutual/shadcnui/dropdown-menu";
import Link from "next/link";
import LoadingContent from "@/components/mutual/loader/component";
const RenderLinkComponent: FC<{ refUrl: string, innerText: string, setisL: (isLoading: boolean) => void }> = ({ refUrl, innerText, setisL }) => {
return (
<Link replace href={refUrl} onClick={() => setisL(true)}>
<DropdownMenuContent className="flex w-48 h-12 align-center justify-center text-center text-md overflow-y-hidden">
<DropdownMenuLabel className="flex items-center justify-center">{innerText}</DropdownMenuLabel>
</DropdownMenuContent>
</Link>
)
}
const RenderLoadingComponent: FC<{ setisL: (isLoading: boolean) => void }> = ({ setisL }) => {
return (
<DropdownMenuContent className="flex w-48 h-12 align-center justify-center text-center text-md overflow-y-hidden">
<DropdownMenuLabel className="flex items-center justify-center">
<LoadingContent height="h-8" size="w-8 h-8" plane="" />
</DropdownMenuLabel>
</DropdownMenuContent>
)
}
const LanguageSelectionItem: React.FC<{
activeLang: string, buttonsLang: string, refUrl: string, innerText: string
}> = ({ activeLang, buttonsLang, refUrl, innerText }) => {
const [isL, setisL] = useState<boolean>(false);
const isC = buttonsLang !== activeLang
const RenderLinkProp = { refUrl, innerText, setisL }
return (
<>{isC && <>{isL ? <RenderLoadingComponent setisL={setisL} /> : <RenderLinkComponent {...RenderLinkProp} />}</>}</>
)
}
export default LanguageSelectionItem

View File

@ -0,0 +1,10 @@
import { Loader2 } from "lucide-react";
const LoadingContent: React.FC<{ height: string, size: string, plane: string }> = ({ height = "h-16", size = "w-36 h-48", plane = "h-full w-full" }) => {
return <>
<div className={`flex items-center justify-center ${plane}`}>
<div className={height}><Loader2 className={`animate-spin ${size}`} /></div>
</div></>
}
export default LoadingContent

View File

@ -0,0 +1,157 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/mutual/shadcnui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,75 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/mutual/shadcnui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
)
}
export { Calendar }

View File

@ -0,0 +1,177 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/mutual/shadcnui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/mutual/shadcnui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,23 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -0,0 +1,88 @@
'use client';
import React, { useState, useEffect } from "react";
import { apiPostFetcher } from "@/lib/fetcher";
import { ApiPaginationRequestWithQuery } from "@/validations/mutual/api/requests/validations";
import { TableComponentProps } from "@/validations/mutual/table/type";
import ComponentTable from "../mutual/TableCardPlain";
import PaginationComponent from "../mutual/UpperPagination";
import LowerPagination from "../mutual/LowerPagination";
const TableCardComponent: React.FC<TableComponentProps> = ({
urls,
schemas,
translations,
columns,
initPagination,
}) => {
const defaultPagination = {
page: initPagination?.page || 1,
size: initPagination?.size || 10,
orderFields: initPagination?.orderFields || [],
orderTypes: initPagination?.orderTypes || [],
query: initPagination?.query || {},
}
const [data, setData] = useState<any>(null);
const [pagination, setPagination] = useState<ApiPaginationRequestWithQuery>(defaultPagination);
const [apiPagination, setApiPagination] = useState<any>({
onPage: 1,
onPageCount: 10,
totalPage: 1,
totalCount: 1,
next: false,
back: false,
});
const handleBack = async () => {
setPagination({ ...pagination, page: pagination.page > 1 ? pagination.page - 1 : pagination.page });
await fetchData();
}
const handleNext = async () => {
setPagination({ ...pagination, page: pagination.page < apiPagination.totalPage ? pagination.page + 1 : pagination.page });
await fetchData();
}
const fetchData = async () => {
const response = await apiPostFetcher({
url: urls.list,
isNoCache: true,
body: {
page: pagination.page,
size: pagination.size,
orderFields: pagination.orderFields,
orderTypes: pagination.orderTypes,
query: pagination.query,
},
});
if (response && response.data) {
setData(response.data.data);
if (response.data.pagination) {
setApiPagination(response.data.pagination);
}
}
};
useEffect(() => { fetchData() }, [pagination.page, pagination.size, pagination.orderFields, pagination.orderTypes]);
const upperPaginationProps = {
apiPagination,
pagination,
setPagination,
defaultPagination,
}
const lowerPaginationProps = {
pagination: apiPagination,
handleBack,
handleNext,
}
const tableProps = {
data,
columns,
translations,
}
return (
<div>
<div className="flex flex-col items-center justify-start my-6"><PaginationComponent {...upperPaginationProps} /></div>
<div className="flex flex-col items-center justify-start my-6"><LowerPagination {...lowerPaginationProps} /></div>
<div className="flex flex-col items-center justify-start my-6"><h1>Post Data Page</h1><ComponentTable {...tableProps} /></div>
</div>
);
}
export default TableCardComponent;

View File

@ -0,0 +1,104 @@
'use client';
import React, { useState, useEffect } from "react";
import ComponentTable from "@/components/mutual/tableView/mutual/TablePlain";
import PaginationComponent from "@/components/mutual/tableView/mutual/UpperPagination";
import LowerPagination from "@/components/mutual/tableView/mutual/LowerPagination";
import { apiPostFetcher } from "@/lib/fetcher";
import { ApiPaginationRequestWithQuery } from "@/validations/mutual/api/requests/validations";
import { TableComponentProps } from "@/validations/mutual/table/type";
const TableComponent: React.FC<TableComponentProps> = ({
urls,
schemas,
translations,
columns,
initPagination,
redirectUrls,
setSelectedRow,
}) => {
const defaultPagination = {
page: initPagination?.page || 1,
size: initPagination?.size || 10,
orderFields: initPagination?.orderFields || [],
orderTypes: initPagination?.orderTypes || [],
query: initPagination?.query || {},
}
const [tableData, setTableData] = useState<any>(null);
const [orgTableData, setOrgTableData] = useState<any>(null);
const [tableColumns, setTableColumns] = useState<string[]>([]);
const [pagination, setPagination] = useState<ApiPaginationRequestWithQuery>(defaultPagination);
const [apiPagination, setApiPagination] = useState<any>({
onPage: 1,
onPageCount: 10,
totalPage: 1,
totalCount: 1,
next: false,
back: false,
});
const handleBack = async () => {
setPagination({ ...pagination, page: pagination.page > 1 ? pagination.page - 1 : pagination.page });
await fetchData();
}
const handleNext = async () => {
setPagination({ ...pagination, page: pagination.page < apiPagination.totalPage ? pagination.page + 1 : pagination.page });
await fetchData();
}
const fetchData = async () => {
const response = await apiPostFetcher({
url: urls.list,
isNoCache: true,
body: {
page: pagination.page,
size: pagination.size,
orderFields: pagination.orderFields,
orderTypes: pagination.orderTypes,
query: pagination.query,
},
});
if (response && response.data) {
const oldData = response.data.data
setOrgTableData(oldData)
if (schemas.table) {
const newData = Object.entries(oldData).map(([key]) => {
return schemas.table.safeParse(oldData[key as keyof typeof oldData]).data
})
setTableData(newData)
setTableColumns(columns.table)
}
if (response.data.pagination) {
setApiPagination(response.data.pagination);
}
}
};
useEffect(() => { fetchData() }, [pagination.page, pagination.size, pagination.orderFields, pagination.orderTypes]);
const upperPaginationProps = {
apiPagination,
pagination,
setPagination,
defaultPagination,
}
const lowerPaginationProps = {
pagination: apiPagination,
handleBack,
handleNext,
}
const tableProps = {
data: tableData,
orgData: orgTableData,
columns: tableColumns,
translations,
redirectUrls,
setSelectedRow,
}
return (
<div>
<div className="flex flex-col items-center justify-start my-6"><PaginationComponent {...upperPaginationProps} /></div>
<div className="flex flex-col items-center justify-start my-6"><LowerPagination {...lowerPaginationProps} /></div>
<div className="flex flex-col items-center justify-start my-6"><ComponentTable {...tableProps} /></div>
</div>
);
}
export default TableComponent;

View File

@ -0,0 +1,40 @@
import React from "react";
import { CreateFormProps } from "@/validations/mutual/forms/type";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form } from "@/components/mutual/shadcnui/form";
import { renderInputsBySchema } from "@/lib/renderInputs";
import { Button } from "@/components/mutual/shadcnui/button";
const CreateForm: React.FC<CreateFormProps> = ({ schemas, labels, selectedRow }) => {
const createSchema = schemas.create
const findLabels = Object.entries(createSchema?.shape || {}).reduce((acc: any, [key, value]: any) => {
acc[key] = {
label: labels[key] || key,
description: value.description,
}
return acc
}, {})
const handleSubmit = (data: any) => {
console.log(data)
}
const form = useForm<FormData>({
resolver: zodResolver(createSchema),
defaultValues: {},
});
return (
<div>
<div>Create Form</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
{renderInputsBySchema(findLabels, form)}
<Button type="submit">Submit</Button>
</form>
</Form>
<div>selectedRow: {JSON.stringify(selectedRow)}</div>
</div>
)
}
export default CreateForm

View File

@ -0,0 +1,24 @@
'use client'
import { Button } from "@/components/mutual/shadcnui/button";
import { LowerPaginationProps } from "@/validations/mutual/pagination/type";
const LowerPagination: React.FC<LowerPaginationProps> = ({
pagination,
handleBack,
handleNext,
}) => {
const paginationBackComponent = pagination.back ? (
<Button className="w-60" onClick={() => handleBack()}>Back</Button>
) : <></>;
const paginationNextComponent = pagination.next ? (
<Button className="w-60" onClick={() => handleNext()}>Next</Button>
) : <></>;
return (
<div className="flex items-center justify-center gap-2 bg-amber-300 p-6 w-full">
{paginationBackComponent}
{paginationNextComponent}
</div>
)
}
export default LowerPagination

View File

@ -0,0 +1,34 @@
import { Input } from "@/components/mutual/shadcnui/input";
import { Label } from "@/components/mutual/shadcnui/label";
import { PaginationDetailsProps } from "@/validations/mutual/pagination/type";
const PaginationDetails: React.FC<PaginationDetailsProps> = ({
pagination,
setPagination,
defaultPagination,
}) => {
return (
<div className="grid grid-cols-6 gap-2 items-center justify-evenly">
<Label>Page Size</Label>
<Input
type="number"
value={pagination.size}
onChange={(e) => setPagination({ ...defaultPagination, size: Number(e.target.value) })}
/>
<Label>Order Field</Label>
<Input
type="text"
value={pagination.orderFields.join(",")}
onChange={(e) => setPagination({ ...defaultPagination, orderFields: e.target.value.split(",") })}
/>
<Label>Order Type</Label>
<Input
type="text"
value={pagination.orderTypes.join(",")}
onChange={(e) => setPagination({ ...pagination, orderType: e.target.value.split(",") })}
/>
</div>
)
}
export default PaginationDetails

View File

@ -0,0 +1,15 @@
import { PaginationShowProps } from "@/validations/mutual/pagination/type"
const PaginationShow: React.FC<PaginationShowProps> = ({ apiPagination }) => {
return (
<div className="grid grid-cols-6 gap-2 items-center justify-evenly">
<p>Page: {apiPagination.onPage}</p>
<p>Page Count: {apiPagination.onPageCount}</p>
<p>Total Page: {apiPagination.totalPage}</p>
<p>Total Count: {apiPagination.totalCount}</p>
<p>Next: {apiPagination.next ? "true" : "false"}</p>
<p>Back: {apiPagination.back ? "true" : "false"}</p>
</div>
)
}
export default PaginationShow

View File

@ -0,0 +1,24 @@
import { Label } from "@/components/mutual/shadcnui/label";
import { Textarea } from "@/components/mutual/shadcnui/textarea";
import { SearchProps } from "@/validations/mutual/table/type";
const SearchBarComponent: React.FC<SearchProps> = ({ pagination, setPagination }) => {
const handleSearch = (query: string) => {
setTimeout(() => {
setPagination({ ...pagination, query: {} })
}, 2000)
}
return (
<div>
<div className="grid grid-cols-1 w-full px-10 py-2">
<Label>Search</Label>
<Textarea
value={JSON.stringify(pagination.query, null, 2)}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</div>
)
}
export default SearchBarComponent

View File

@ -0,0 +1,47 @@
'use client'
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from "@/components/mutual/shadcnui/card";
import { TableCardProps } from "@/validations/mutual/table/type";
import { useEffect, useState } from "react";
const ComponentTableCardPlain: React.FC<TableCardProps> = ({ data, columns, translations }) => {
const [tableData, setTableData] = useState<any>(data);
useEffect(() => {
setTableData(data);
}, [data]);
const renderCards = () => {
return tableData?.map((item: any, index: number) => (
<Card className="w-full min-w-full my-2 p-5" key={index}>
<CardHeader>
<CardTitle>Row : {index + 1}</CardTitle>
<CardDescription>{item.uuid}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-start justify-start">Email: {item.email}</div>
<div className="flex flex-col items-start justify-start">Phone Number: {item.phoneNumber}</div>
<div>Created At: {item.createdAt}</div>
</CardContent>
</Card>
))
}
const noDataFound = () => (
<>
No Data Found
</>
)
return (
<>
{
tableData ? (
<div className="w-full min-w-full">
<div className="flex flex-col items-center justify-start my-6">{renderCards()}</div>
</div>
) : (noDataFound())
}
</>
)
}
export default ComponentTableCardPlain

View File

@ -0,0 +1,60 @@
'use client'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/mutual/shadcnui/table";
import { useEffect, useState } from "react";
import { TableProps } from "@/validations/mutual/table/type";
const ComponentTablePlain: React.FC<TableProps> = ({
data, orgData, columns, translations, redirectUrls, setSelectedRow
}) => {
const [tableData, setTableData] = useState<any>(data);
useEffect(() => {
setTableData(data);
}, [data]);
const renderColumns = () => {
return [translations?.rows, ...columns].map((column, index) => {
return (
<TableHead key={`headers-${index}`}>{column}</TableHead>
)
})
}
const renderRows = () => (
<>{tableData?.map((item: any, index: number) => {
return (
<TableRow key={`${index}-row`} >
<TableCell>{index + 1}</TableCell>
{
Object.entries(item).map(([key, value]: [string, any]) => (
<TableCell key={`${index}-column-${key}`}>{value}</TableCell>
))
}
{
Object.values(redirectUrls?.table || {}).map((redirectUrl: any, index: number) => {
return (
<TableCell className="cursor-pointer w-4" key={`${index}-action-${index}`}
onClick={() => setSelectedRow?.(orgData[index])}>{redirectUrl}</TableCell>
)
})
}
</TableRow>
)
})}</>
)
const noDataFound = (<>No Data Found</>)
const renderTable = (
<Table><TableHeader><TableRow>{renderColumns()}</TableRow></TableHeader><TableBody>{renderRows()}</TableBody></Table>
)
return (
<>
<div className="flex flex-row justify-between gap-2">
{Object.values(redirectUrls?.page || {}).map((action, index) => (
<div className="flex flex-row justify-center items-center gap-2" key={`page-action-${index}`}>{action}</div>
))}
</div>
{tableData ? renderTable : noDataFound}
</>
)
}
export default ComponentTablePlain

View File

@ -0,0 +1,46 @@
'use client'
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { UpdateFormProps } from "@/validations/mutual/forms/type";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form } from "@/components/mutual/shadcnui/form";
import { renderInputsBySchema } from "@/lib/renderInputs";
import { Button } from "@/components/mutual/shadcnui/button";
const UpdateForm: React.FC<UpdateFormProps> = ({ schemas, selectedRow, rollbackTo, labels }) => {
const router = useRouter()
useEffect(() => {
if (!selectedRow) {
router.push(rollbackTo, { scroll: false })
}
}, [selectedRow])
const updateSchema = schemas.update
const findLabels = Object.entries(updateSchema?.shape || {}).reduce((acc: any, [key, value]: any) => {
acc[key] = {
label: labels[key] || key,
description: value.description,
}
return acc
}, {})
const handleSubmit = (data: any) => {
console.log(data)
}
const form = useForm<FormData>({
resolver: zodResolver(updateSchema),
defaultValues: selectedRow,
});
return (
<div>
<div>Update Form</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
{selectedRow && renderInputsBySchema(findLabels, form)}
{selectedRow && <Button type="submit">Submit</Button>}
</form>
</Form>
</div>
)
}
export default UpdateForm

Some files were not shown because too many files have changed in this diff Show More