login setup client added
This commit is contained in:
parent
015a84ccf6
commit
b15f58c319
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
'use server';
|
||||
import Login from "@/webPages/auth/Login/page";
|
||||
|
||||
const LoginPage = async () => { return <Login language="en" /> };
|
||||
|
||||
export default LoginPage;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Export all utility functions from a single entry point
|
||||
export * from './responseHandlers';
|
||||
export * from './requestHandlers';
|
||||
export * from './apiOperations';
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
interface IntrerfaceLayerDropdown {
|
||||
isActive: boolean;
|
||||
innerText: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Reference in New Issue