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_modules /api_modules
|
||||||
|
|
||||||
COPY /api_services/api_middlewares /api_middlewares
|
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/endpoints /api_initializer/endpoints
|
||||||
COPY /api_services/api_builds/auth-service/events /api_initializer/events
|
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/validations /api_initializer/validations
|
||||||
COPY /api_services/api_builds/auth-service/index.py /api_initializer/index.py
|
# COPY /api_services/api_builds/auth_service/index.py /api_initializer/index.py
|
||||||
|
|
||||||
# Set Python path to include app directory
|
# Set Python path to include app directory
|
||||||
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
|
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ from validations.request.auth.validations import (
|
||||||
RequestForgotPasswordPhone,
|
RequestForgotPasswordPhone,
|
||||||
RequestForgotPasswordEmail,
|
RequestForgotPasswordEmail,
|
||||||
RequestVerifyOTP,
|
RequestVerifyOTP,
|
||||||
RequestApplication,
|
|
||||||
)
|
)
|
||||||
from events.auth.events import AuthHandlers
|
from events.auth.events import AuthHandlers
|
||||||
from endpoints.index import endpoints_index
|
from endpoints.index import endpoints_index
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
from typing import Any, Union
|
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_controllers.redis.database import RedisActions
|
||||||
|
from api_modules.token.password_module import PasswordModule
|
||||||
|
|
||||||
from schemas import Users
|
from schemas import Users
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,7 +33,7 @@ class RedisHandlers:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_object_from_redis(cls, access_token: str) -> TokenDictType:
|
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:
|
if not redis_response.status:
|
||||||
raise ValueError("EYS_0001")
|
raise ValueError("EYS_0001")
|
||||||
if redis_object := redis_response.first:
|
if redis_object := redis_response.first:
|
||||||
|
|
@ -33,21 +42,21 @@ class RedisHandlers:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_object_to_redis(cls, user: Users, token, header_info):
|
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()
|
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})
|
RedisActions.set_json(list_keys=keys, value={**token, **header_info}, expires={"hours": 1, "minutes": 30})
|
||||||
return generated_access_token
|
return generated_access_token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_token_at_redis(cls, token: str, add_payload: Union[CompanyToken, OccupantToken]):
|
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)
|
already_token = cls.process_redis_object(already_token_data)
|
||||||
if already_token.is_employee and isinstance(add_payload, CompanyToken):
|
if already_token.is_employee and isinstance(add_payload, CompanyToken):
|
||||||
already_token.selected_company = add_payload
|
already_token.selected_company = add_payload
|
||||||
elif already_token.is_occupant and isinstance(add_payload, OccupantToken):
|
elif already_token.is_occupant and isinstance(add_payload, OccupantToken):
|
||||||
already_token.selected_occupant = add_payload
|
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})
|
result = RedisActions.set_json(list_keys=list_keys, value=already_token.model_dump(), expires={"hours": 1, "minutes": 30})
|
||||||
return result.first
|
return result.first
|
||||||
raise ValueError("Something went wrong")
|
raise ValueError("Something went wrong")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
services:
|
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:
|
auth_service:
|
||||||
container_name: auth_service
|
container_name: auth_service
|
||||||
build:
|
build:
|
||||||
|
|
@ -6,6 +38,8 @@ services:
|
||||||
dockerfile: api_services/api_builds/auth_service/Dockerfile
|
dockerfile: api_services/api_builds/auth_service/Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
|
networks:
|
||||||
|
- wag-services
|
||||||
environment:
|
environment:
|
||||||
- API_PATH=app:app
|
- API_PATH=app:app
|
||||||
- API_HOST=0.0.0.0
|
- API_HOST=0.0.0.0
|
||||||
|
|
@ -28,6 +62,8 @@ services:
|
||||||
dockerfile: api_services/api_builds/restriction_service/Dockerfile
|
dockerfile: api_services/api_builds/restriction_service/Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
|
networks:
|
||||||
|
- wag-services
|
||||||
environment:
|
environment:
|
||||||
- API_PATH=app:app
|
- API_PATH=app:app
|
||||||
- API_HOST=0.0.0.0
|
- API_HOST=0.0.0.0
|
||||||
|
|
@ -49,6 +85,8 @@ services:
|
||||||
dockerfile: api_services/api_builds/management_service/Dockerfile
|
dockerfile: api_services/api_builds/management_service/Dockerfile
|
||||||
env_file:
|
env_file:
|
||||||
- api_env.env
|
- api_env.env
|
||||||
|
networks:
|
||||||
|
- wag-services
|
||||||
environment:
|
environment:
|
||||||
- API_PATH=app:app
|
- API_PATH=app:app
|
||||||
- API_HOST=0.0.0.0
|
- 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