updated service web customer
This commit is contained in:
parent
1c0bb741a0
commit
3a35752b46
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { checkAccessTokenIsValid } from "@/apifetchers/mutual/cookies/token";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const token = await checkAccessTokenIsValid();
|
||||||
|
if (token) {
|
||||||
|
return NextResponse.json({ status: 200 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking token:", error);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ status: 401 });
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { removeAllAccessCookies } from "@/apifetchers/mutual/cookies/token";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
await removeAllAccessCookies();
|
||||||
|
return NextResponse.json({ status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking token:", error);
|
||||||
|
return NextResponse.json({ status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
getSelectionFromRedis,
|
||||||
|
setSelectionToRedis,
|
||||||
|
} from "@/fetchers/custom/context/dash/selection/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const selection = await getSelectionFromRedis();
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: selection || null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const selection = await request.json();
|
||||||
|
await setSelectionToRedis(selection);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: selection || null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
getSettingsFromRedis,
|
||||||
|
setSettingsToRedis,
|
||||||
|
} from "@/fetchers/custom/context/dash/settings/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const settings = await getSettingsFromRedis();
|
||||||
|
return NextResponse.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const settings = await request.json();
|
||||||
|
await setSettingsToRedis(settings);
|
||||||
|
return NextResponse.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
getUserFromRedis,
|
||||||
|
setUserToRedis,
|
||||||
|
} from "@/fetchers/custom/context/dash/user/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getUserFromRedis();
|
||||||
|
return NextResponse.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await request.json();
|
||||||
|
await setUserToRedis(user);
|
||||||
|
return NextResponse.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
getConfigFromRedis,
|
||||||
|
setConfigToRedis,
|
||||||
|
} from "@/fetchers/custom/context/page/config/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const config = await getConfigFromRedis();
|
||||||
|
return NextResponse.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const config = await request.json();
|
||||||
|
await setConfigToRedis(config);
|
||||||
|
return NextResponse.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: "Internal server error" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
getMenuFromRedis,
|
||||||
|
setMenuToRedis,
|
||||||
|
} from "@/fetchers/custom/context/page/menu/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const menu = await getMenuFromRedis();
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: menu,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const menu = await request.json();
|
||||||
|
await setMenuToRedis(menu);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: menu,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
getOnlineFromRedis,
|
||||||
|
setOnlineToRedis,
|
||||||
|
} from "@/fetchers/custom/context/page/online/fetch";
|
||||||
|
import { AuthError } from "@/fetchers/types/context";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const online = await getOnlineFromRedis();
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: online,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
// Return 401 Unauthorized for authentication errors
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// For other errors, return 500 Internal Server Error
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const online = await request.json();
|
||||||
|
await setOnlineToRedis(online);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 200,
|
||||||
|
data: online,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AuthError) {
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 401,
|
||||||
|
error: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 500,
|
||||||
|
error: "Internal server error",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { loginViaAccessKeys } from "@/apifetchers/custom/login/login";
|
||||||
|
import { loginSchemaEmail } from "@/webPages/auth/login/schemas";
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.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,15 @@
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const data = await result.json();
|
||||||
|
return NextResponse.json({ status: 200, data: data });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ status: 500, message: "No data is found" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,40 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { loginSelectEmployee } from "@/apifetchers/custom/login/login";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const loginSchemaEmployee = z.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.headers;
|
||||||
|
const body = await req.json();
|
||||||
|
const dataValidated = {
|
||||||
|
uuid: body.uuid,
|
||||||
|
};
|
||||||
|
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,40 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { loginSelectOccupant } from "@/apifetchers/custom/login/login";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const loginSchemaOccupant = z.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request): Promise<NextResponse> {
|
||||||
|
try {
|
||||||
|
const headers = req.headers;
|
||||||
|
const body = await req.json();
|
||||||
|
const dataValidated = {
|
||||||
|
uuid: body.uuid,
|
||||||
|
};
|
||||||
|
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,165 @@
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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[] = []
|
||||||
|
) {
|
||||||
|
// 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) {
|
||||||
|
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>;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DashboardLayout } from '@/layouts/dashboard/layout';
|
||||||
|
import { LanguageTypes } from '@/validations/mutual/language/validations';
|
||||||
|
|
||||||
|
export default function Page({ params, searchParams }: { params: { page?: string[] }, searchParams: Record<string, any> }) {
|
||||||
|
const lang: LanguageTypes = 'en';
|
||||||
|
return <DashboardLayout params={params} searchParams={searchParams} lang={lang} />;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "../styles/custom-scrollbar.css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { FC, useState, useRef, useEffect } from "react";
|
||||||
|
import renderOneClientSelection from "./renderOneClientSelection";
|
||||||
|
|
||||||
|
interface ClientSelectionSectionProps {
|
||||||
|
selectionData: any;
|
||||||
|
initialSelectedClient?: any;
|
||||||
|
onClientSelect?: (client: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientSelectionSection: FC<ClientSelectionSectionProps> = ({
|
||||||
|
selectionData,
|
||||||
|
initialSelectedClient = null,
|
||||||
|
onClientSelect
|
||||||
|
}) => {
|
||||||
|
const [selectedClient, setSelectedClient] = useState<any>(initialSelectedClient);
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// If there's only one client or no clients, don't show dropdown functionality
|
||||||
|
const shouldShowAsDropdown = selectionData?.selectionList?.length > 1;
|
||||||
|
|
||||||
|
if (!selectionData || !selectionData.selectionList || selectionData.selectionList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClientSelection = (client: any) => {
|
||||||
|
setSelectedClient(client);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
if (onClientSelect) {
|
||||||
|
onClientSelect(client);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3 relative" ref={dropdownRef}>
|
||||||
|
{/* Current selection that acts as dropdown trigger */}
|
||||||
|
<div onClick={() => shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen)}>
|
||||||
|
{selectedClient && renderOneClientSelection({
|
||||||
|
item: selectedClient,
|
||||||
|
isSelected: true,
|
||||||
|
onClickHandler: () => shouldShowAsDropdown && setIsDropdownOpen(!isDropdownOpen)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dropdown menu */}
|
||||||
|
{shouldShowAsDropdown && isDropdownOpen && (
|
||||||
|
<div className="absolute left-0 right-0 top-full mt-1 bg-white rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto">
|
||||||
|
{selectionData.selectionList
|
||||||
|
.filter((client: any) => client.uu_id !== selectedClient?.uu_id)
|
||||||
|
.map((client: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={client.uu_id || client.id || `client-${index}`}
|
||||||
|
onClick={() => handleClientSelection(client)}
|
||||||
|
>
|
||||||
|
{renderOneClientSelection({
|
||||||
|
item: client,
|
||||||
|
isSelected: false,
|
||||||
|
onClickHandler: handleClientSelection
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientSelectionSection;
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface TableColumn {
|
||||||
|
header: string;
|
||||||
|
accessor: string;
|
||||||
|
cell?: (value: any, row: any) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataTableProps {
|
||||||
|
title: string;
|
||||||
|
columns: TableColumn[];
|
||||||
|
data: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DataTable({ title, columns, data }: DataTableProps) {
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const toggleDropdown = (index: number) => {
|
||||||
|
setActiveDropdown(prev => prev === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md">
|
||||||
|
<div className="flex justify-between mb-4 items-start">
|
||||||
|
<div className="font-medium">{title}</div>
|
||||||
|
<div className="dropdown relative">
|
||||||
|
<button type="button" className="dropdown-toggle text-gray-400 hover:text-gray-600">
|
||||||
|
<i className="ri-more-fill"></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 hidden py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px]">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full min-w-[540px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<th key={index} className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left">
|
||||||
|
{column.header}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((row, rowIndex) => (
|
||||||
|
<tr key={rowIndex}>
|
||||||
|
{columns.map((column, colIndex) => (
|
||||||
|
<td key={colIndex} className="py-2 px-4 border-b border-b-gray-50">
|
||||||
|
{column.cell ? column.cell(row[column.accessor], row) : row[column.accessor]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||||
|
<div className="dropdown relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleDropdown(rowIndex)}
|
||||||
|
className="dropdown-toggle text-gray-400 hover:text-gray-600 text-sm w-6 h-6 rounded flex items-center justify-center bg-gray-50"
|
||||||
|
>
|
||||||
|
<i className="ri-more-2-fill"></i>
|
||||||
|
</button>
|
||||||
|
{activeDropdown === rowIndex && (
|
||||||
|
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface EarningItem {
|
||||||
|
service: string;
|
||||||
|
imageUrl: string;
|
||||||
|
amount: string;
|
||||||
|
isPositive: boolean;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EarningsTableProps {
|
||||||
|
items: EarningItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EarningsTable({ items }: EarningsTableProps) {
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
setActiveDropdown(!activeDropdown);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md">
|
||||||
|
<div className="flex justify-between mb-4 items-start">
|
||||||
|
<div className="font-medium">Earnings</div>
|
||||||
|
<div className="dropdown relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
className="dropdown-toggle text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<i className="ri-more-fill"></i>
|
||||||
|
</button>
|
||||||
|
{activeDropdown && (
|
||||||
|
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full min-w-[460px]">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left rounded-tl-md rounded-bl-md">Service</th>
|
||||||
|
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left">Earning</th>
|
||||||
|
<th className="text-[12px] uppercase tracking-wide font-medium text-gray-400 py-2 px-4 bg-gray-50 text-left rounded-tr-md rounded-br-md">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={item.imageUrl} alt="" className="w-8 h-8 rounded object-cover block" />
|
||||||
|
<a href="#" className="text-gray-600 text-sm font-medium hover:text-blue-500 ml-2 truncate">{item.service}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||||
|
<span className={`text-[13px] font-medium ${item.isPositive ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||||
|
{item.isPositive ? '+' : '-'}{item.amount}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-4 border-b border-b-gray-50">
|
||||||
|
<span className={`inline-block p-1 rounded ${item.isPositive ? 'bg-emerald-500/10 text-emerald-500' : 'bg-rose-500/10 text-rose-500'} font-medium text-[12px] leading-none`}>
|
||||||
|
{item.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface OrderStatsProps {
|
||||||
|
activeOrders: number;
|
||||||
|
activeAmount: number;
|
||||||
|
completedOrders: number;
|
||||||
|
completedAmount: number;
|
||||||
|
canceledOrders: number;
|
||||||
|
canceledAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrderStats({
|
||||||
|
activeOrders,
|
||||||
|
activeAmount,
|
||||||
|
completedOrders,
|
||||||
|
completedAmount,
|
||||||
|
canceledOrders,
|
||||||
|
canceledAmount
|
||||||
|
}: OrderStatsProps) {
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<boolean>(false);
|
||||||
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
setActiveDropdown(!activeDropdown);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chart initialization would go here
|
||||||
|
useEffect(() => {
|
||||||
|
if (chartRef.current) {
|
||||||
|
const ctx = chartRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
// This is where you would initialize a chart library like Chart.js
|
||||||
|
// For now, we'll just draw some placeholder text
|
||||||
|
ctx.font = '14px Arial';
|
||||||
|
ctx.fillStyle = '#333';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Order Statistics Chart', chartRef.current.width / 2, chartRef.current.height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-gray-100 shadow-md shadow-black/5 p-6 rounded-md lg:col-span-2">
|
||||||
|
<div className="flex justify-between mb-4 items-start">
|
||||||
|
<div className="font-medium">Order Statistics</div>
|
||||||
|
<div className="dropdown relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
className="dropdown-toggle text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<i className="ri-more-fill"></i>
|
||||||
|
</button>
|
||||||
|
{activeDropdown && (
|
||||||
|
<ul className="dropdown-menu shadow-md shadow-black/5 z-30 py-1.5 rounded-md bg-white border border-gray-100 w-full max-w-[140px] absolute right-0">
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" className="flex items-center text-[13px] py-1.5 px-4 text-gray-600 hover:text-blue-500 hover:bg-gray-50">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||||
|
<div className="flex items-center mb-0.5">
|
||||||
|
<div className="text-xl font-semibold">{activeOrders}</div>
|
||||||
|
<span className="p-1 rounded text-[12px] font-semibold bg-blue-500/10 text-blue-500 leading-none ml-1">${activeAmount}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400 text-sm">Active</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||||
|
<div className="flex items-center mb-0.5">
|
||||||
|
<div className="text-xl font-semibold">{completedOrders}</div>
|
||||||
|
<span className="p-1 rounded text-[12px] font-semibold bg-emerald-500/10 text-emerald-500 leading-none ml-1">+${completedAmount}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400 text-sm">Completed</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-gray-200 p-4">
|
||||||
|
<div className="flex items-center mb-0.5">
|
||||||
|
<div className="text-xl font-semibold">{canceledOrders}</div>
|
||||||
|
<span className="p-1 rounded text-[12px] font-semibold bg-rose-500/10 text-rose-500 leading-none ml-1">-${canceledAmount}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400 text-sm">Canceled</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas ref={chartRef} id="order-chart" width="100%" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import ClientSelectionSection from './ClientSelectionSection';
|
||||||
|
import UserProfileSection from './UserProfileSection';
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
isSidebarOpen: boolean;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({ isSidebarOpen, toggleSidebar }: SidebarProps) {
|
||||||
|
const [openSidebarSubmenus, setOpenSidebarSubmenus] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// Sample user data for demonstration
|
||||||
|
const sampleUserData = {
|
||||||
|
email: 'user@example.com',
|
||||||
|
person: {
|
||||||
|
firstname: 'John',
|
||||||
|
surname: 'Doe'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleOnlineData = {
|
||||||
|
userType: 'admin'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sample client selection data
|
||||||
|
const sampleClientData = {
|
||||||
|
selectionList: [
|
||||||
|
{
|
||||||
|
uu_id: '1',
|
||||||
|
name: 'Acme Corp',
|
||||||
|
description: 'Technology solutions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uu_id: '2',
|
||||||
|
name: 'Globex',
|
||||||
|
description: 'Manufacturing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uu_id: '3',
|
||||||
|
name: 'Initech',
|
||||||
|
description: 'Software development'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle submenu
|
||||||
|
const toggleSubmenu = (itemName: string) => {
|
||||||
|
setOpenSidebarSubmenus(prev => ({ ...prev, [itemName]: !prev[itemName] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`fixed left-0 top-0 w-72 h-full bg-[#f8f4f3] z-50 sidebar-menu transition-transform ${isSidebarOpen ? '' : '-translate-x-full'} md:translate-x-0 flex flex-col`}>
|
||||||
|
{/* Fixed header section */}
|
||||||
|
<div className="p-4 border-b border-b-gray-800 flex-shrink-0">
|
||||||
|
<UserProfileSection userData={sampleUserData} onlineData={sampleOnlineData} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable content section */}
|
||||||
|
<div className="flex-grow overflow-y-auto custom-scrollbar p-4">
|
||||||
|
{/* Client selection menu */}
|
||||||
|
<ClientSelectionSection
|
||||||
|
selectionData={sampleClientData}
|
||||||
|
initialSelectedClient={sampleClientData.selectionList[0]}
|
||||||
|
onClientSelect={(client) => console.log('Selected client:', client)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Add more scrollable content here if needed */}
|
||||||
|
<ul className="mt-4">
|
||||||
|
<span className="text-gray-400 font-bold">ADMIN</span>
|
||||||
|
<li className="mb-1 group active">
|
||||||
|
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||||
|
<i className="ri-home-2-line mr-3 text-lg"></i>
|
||||||
|
<span className="text-sm">Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className={`mb-1 group ${openSidebarSubmenus['users'] ? 'selected' : ''}`}>
|
||||||
|
<a href="#" onClick={(e) => { e.preventDefault(); toggleSubmenu('users'); }} className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100 sidebar-dropdown-toggle">
|
||||||
|
<i className='bx bx-user mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Users</span>
|
||||||
|
<i className={`ri-arrow-right-s-line ml-auto ${openSidebarSubmenus['users'] ? 'rotate-90' : ''}`}></i>
|
||||||
|
</a>
|
||||||
|
<ul className={`pl-7 mt-2 ${openSidebarSubmenus['users'] ? 'block' : 'hidden'}`}>
|
||||||
|
<li className="mb-4">
|
||||||
|
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">All</a>
|
||||||
|
</li>
|
||||||
|
<li className="mb-4">
|
||||||
|
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Roles</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li className="mb-1 group">
|
||||||
|
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||||
|
<i className='bx bx-list-ul mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Activities</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<span className="text-gray-400 font-bold">BLOG</span>
|
||||||
|
<li className={`mb-1 group ${openSidebarSubmenus['post'] ? 'selected' : ''}`}>
|
||||||
|
<a href="#" onClick={(e) => { e.preventDefault(); toggleSubmenu('post'); }} className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100 sidebar-dropdown-toggle">
|
||||||
|
<i className='bx bxl-blogger mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Post</span>
|
||||||
|
<i className={`ri-arrow-right-s-line ml-auto ${openSidebarSubmenus['post'] ? 'rotate-90' : ''}`}></i>
|
||||||
|
</a>
|
||||||
|
<ul className={`pl-7 mt-2 ${openSidebarSubmenus['post'] ? 'block' : 'hidden'}`}>
|
||||||
|
<li className="mb-4">
|
||||||
|
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">All</a>
|
||||||
|
</li>
|
||||||
|
<li className="mb-4">
|
||||||
|
<a href="#" className="text-gray-900 text-sm flex items-center hover:text-[#f84525] before:content-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Categories</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="mb-1 group">
|
||||||
|
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||||
|
<i className='bx bx-archive mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Archive</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<span className="text-gray-400 font-bold">PERSONAL</span>
|
||||||
|
<li className="mb-1 group">
|
||||||
|
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||||
|
<i className='bx bx-bell mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Notifications</span>
|
||||||
|
<span className="md:block px-2 py-0.5 ml-auto text-xs font-medium tracking-wide text-red-600 bg-red-200 rounded-full">5</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="mb-1 group">
|
||||||
|
<a href="#" className="flex font-semibold items-center py-2 px-4 text-gray-900 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-[.selected]:bg-gray-950 group-[.selected]:text-gray-100">
|
||||||
|
<i className='bx bx-envelope mr-3 text-lg'></i>
|
||||||
|
<span className="text-sm">Messages</span>
|
||||||
|
<span className="md:block px-2 py-0.5 ml-auto text-xs font-medium tracking-wide text-green-600 bg-green-200 rounded-full">2 New</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Overlay for mobile */}
|
||||||
|
{isSidebarOpen && (
|
||||||
|
<div className="fixed top-0 left-0 w-full h-full bg-black/50 z-40 md:hidden sidebar-overlay" onClick={toggleSidebar}></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
interface StatCardProps {
|
||||||
|
title: string;
|
||||||
|
count: string;
|
||||||
|
icon: string;
|
||||||
|
iconColor: string;
|
||||||
|
percentage: string;
|
||||||
|
isPositive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatCard({ title, count, icon, iconColor, percentage, isPositive }: StatCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-md border border-gray-100 shadow-md shadow-black/5">
|
||||||
|
<div className="flex justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center mb-1">
|
||||||
|
<div className="text-2xl font-semibold">{count}</div>
|
||||||
|
<div className={`text-xs font-medium ${isPositive ? 'text-green-500' : 'text-red-500'} ml-2`}>
|
||||||
|
{isPositive ? '+' : '-'}{percentage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-gray-400">{title}</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${iconColor} bg-opacity-10 w-12 h-12 rounded-full flex items-center justify-center`}>
|
||||||
|
<i className={`${icon} text-2xl`}></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`text-xs font-medium ${isPositive ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{percentage} {isPositive ? 'increase' : 'decrease'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 ml-2">from last week</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
interface UserProfileSectionProps {
|
||||||
|
userData: {
|
||||||
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
|
person?: {
|
||||||
|
firstname: string;
|
||||||
|
surname: string;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
onlineData?: {
|
||||||
|
userType?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfileSection: FC<UserProfileSectionProps> = ({ userData, onlineData }) => {
|
||||||
|
if (!userData) return null;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md">
|
||||||
|
<div className="bg-amber-300 p-2 hover:bg-amber-400 transition-all">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2">
|
||||||
|
{userData && userData.avatar ? (
|
||||||
|
<img className="rounded-full border border-white" src={userData.avatar} alt="Avatar" width={40} height={40} />
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-amber-400 flex items-center justify-center border border-white">
|
||||||
|
<div className="text-white text-sm font-bold">{userData?.email ? userData.email.slice(0, 2).toUpperCase() : 'U'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<h2 className="text-sm font-bold text-black truncate">{userData?.person ? `${userData.person.firstname} ${userData.person.surname}` : 'User'}</h2>
|
||||||
|
<p className="text-xs text-amber-800 truncate">{userData?.email || 'No email'}</p>
|
||||||
|
<p className="text-xs font-medium capitalize">{onlineData?.userType || 'guest'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfileSection;
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { ContentProps } from '@/validations/mutual/dashboard/props';
|
||||||
|
import StatCard from '@/components/custom/StatCard';
|
||||||
|
import DataTable from '@/components/custom/DataTable';
|
||||||
|
import OrderStats from '@/components/custom/OrderStats';
|
||||||
|
import EarningsTable from '@/components/custom/EarningsTable';
|
||||||
|
|
||||||
|
interface DashboardPageProps {
|
||||||
|
searchParams: Record<string, any>;
|
||||||
|
userData?: any;
|
||||||
|
userLoading?: boolean;
|
||||||
|
userError?: any;
|
||||||
|
refreshUser?: () => void;
|
||||||
|
updateUser?: (data: any) => void;
|
||||||
|
onlineData?: any;
|
||||||
|
onlineLoading?: boolean;
|
||||||
|
onlineError?: any;
|
||||||
|
refreshOnline?: () => void;
|
||||||
|
updateOnline?: (data: any) => void;
|
||||||
|
activePageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardPage: FC<DashboardPageProps> = ({
|
||||||
|
searchParams,
|
||||||
|
userData,
|
||||||
|
userLoading,
|
||||||
|
onlineData,
|
||||||
|
activePageUrl
|
||||||
|
}) => {
|
||||||
|
// Sample data for the dashboard
|
||||||
|
const statCardsData = [
|
||||||
|
{
|
||||||
|
title: 'Users',
|
||||||
|
count: '36.5k',
|
||||||
|
icon: 'bx bx-user',
|
||||||
|
iconColor: 'text-blue-500',
|
||||||
|
percentage: '4.65%',
|
||||||
|
isPositive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Companies',
|
||||||
|
count: '4.5k',
|
||||||
|
icon: 'bx bx-building',
|
||||||
|
iconColor: 'text-yellow-500',
|
||||||
|
percentage: '1.25%',
|
||||||
|
isPositive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Blogs',
|
||||||
|
count: '12.5k',
|
||||||
|
icon: 'bx bxl-blogger',
|
||||||
|
iconColor: 'text-green-500',
|
||||||
|
percentage: '2.15%',
|
||||||
|
isPositive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Revenue',
|
||||||
|
count: '$35.5k',
|
||||||
|
icon: 'bx bx-dollar',
|
||||||
|
iconColor: 'text-pink-500',
|
||||||
|
percentage: '3.75%',
|
||||||
|
isPositive: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const userRolesColumns = [
|
||||||
|
{ header: 'Name', accessor: 'name' },
|
||||||
|
{ header: 'Email', accessor: 'email' },
|
||||||
|
{
|
||||||
|
header: 'Role', accessor: 'role',
|
||||||
|
cell: (value: string) => (
|
||||||
|
<span className={`py-1 px-2 rounded-md text-xs ${value === 'Admin' ? 'bg-blue-500/10 text-blue-500' : value === 'Editor' ? 'bg-yellow-500/10 text-yellow-500' : 'bg-emerald-500/10 text-emerald-500'}`}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const userRolesData = [
|
||||||
|
{ name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
||||||
|
{ name: 'Jane Smith', email: 'jane@example.com', role: 'Editor' },
|
||||||
|
{ name: 'Robert Johnson', email: 'robert@example.com', role: 'Customer' },
|
||||||
|
{ name: 'Emily Davis', email: 'emily@example.com', role: 'Customer' },
|
||||||
|
{ name: 'Michael Brown', email: 'michael@example.com', role: 'Editor' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const activitiesColumns = [
|
||||||
|
{ header: 'Name', accessor: 'name' },
|
||||||
|
{ header: 'Date', accessor: 'date' },
|
||||||
|
{ header: 'Time', accessor: 'time' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const activitiesData = [
|
||||||
|
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||||
|
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||||
|
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||||
|
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' },
|
||||||
|
{ name: 'Lorem Ipsum', date: '02-02-2024', time: '17.45' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const earningsData = [
|
||||||
|
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
|
||||||
|
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
|
||||||
|
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' },
|
||||||
|
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: false, status: 'Withdrawn' },
|
||||||
|
{ service: 'Create landing page', imageUrl: 'https://placehold.co/32x32', amount: '$235', isPositive: true, status: 'Pending' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* User info section */}
|
||||||
|
{userData && (
|
||||||
|
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">User Information</h3>
|
||||||
|
<p>User Type: {userData.user_tag || 'N/A'}</p>
|
||||||
|
{userData.person && (
|
||||||
|
<p>Name: {userData.person.firstname} {userData.person.surname}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stat cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||||
|
{statCardsData.map((card, index) => (
|
||||||
|
<StatCard
|
||||||
|
key={index}
|
||||||
|
title={card.title}
|
||||||
|
count={card.count}
|
||||||
|
icon={card.icon}
|
||||||
|
iconColor={card.iconColor}
|
||||||
|
percentage={card.percentage}
|
||||||
|
isPositive={card.isPositive}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tables */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<DataTable
|
||||||
|
title="User Roles"
|
||||||
|
columns={userRolesColumns}
|
||||||
|
data={userRolesData}
|
||||||
|
/>
|
||||||
|
<DataTable
|
||||||
|
title="Activities"
|
||||||
|
columns={activitiesColumns}
|
||||||
|
data={activitiesData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Order Stats and Earnings */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
|
<OrderStats
|
||||||
|
activeOrders={10}
|
||||||
|
activeAmount={80}
|
||||||
|
completedOrders={50}
|
||||||
|
completedAmount={469}
|
||||||
|
canceledOrders={4}
|
||||||
|
canceledAmount={130}
|
||||||
|
/>
|
||||||
|
<EarningsTable items={earningsData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ContentProps } from "@/validations/mutual/dashboard/props";
|
||||||
|
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
|
||||||
|
import pageIndexMulti from "@/pages/multi/index";
|
||||||
|
|
||||||
|
const PageToBeChildrendMulti: React.FC<ContentProps> = ({
|
||||||
|
activePageUrl,
|
||||||
|
userData,
|
||||||
|
userLoading,
|
||||||
|
userError,
|
||||||
|
onlineData,
|
||||||
|
onlineLoading,
|
||||||
|
onlineError,
|
||||||
|
searchParams,
|
||||||
|
refreshOnline,
|
||||||
|
updateOnline,
|
||||||
|
refreshUser,
|
||||||
|
updateUser,
|
||||||
|
}) => {
|
||||||
|
const pageComponents = pageIndexMulti[activePageUrl];
|
||||||
|
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
||||||
|
const ComponentKey = Object.keys(pageComponents)[0];
|
||||||
|
const PageComponent = pageComponents[ComponentKey];
|
||||||
|
if (!PageComponent) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
||||||
|
return <PageComponent
|
||||||
|
activePageUrl={activePageUrl} searchParams={searchParams}
|
||||||
|
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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> = async ({ lang, translations, activePageUrl, mode }) => {
|
||||||
|
const ApplicationToRender = await resolveWhichPageToRenderSingle({ activePageUrl })
|
||||||
|
if (ApplicationToRender) {
|
||||||
|
return <ApplicationToRender lang={lang} translations={translations} activePageUrl={activePageUrl} mode={mode} />
|
||||||
|
}
|
||||||
|
return <ContentToRenderNoPage lang={lang} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageToBeChildrendSingle
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
'use client';
|
||||||
|
import { FC, Suspense, memo } from "react";
|
||||||
|
import { ContentProps, ModeTypes, ModeTypesList } from "@/validations/mutual/dashboard/props";
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
import PageToBeChildrendMulti from "./PageToBeChildrendMulti";
|
||||||
|
import LoadingContent from "@/components/mutual/loader/component";
|
||||||
|
|
||||||
|
const MemoizedMultiPage = memo(PageToBeChildrendMulti);
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
errorLoadingContent: "Error Loading Content",
|
||||||
|
contentArea: "Content Area",
|
||||||
|
contentLoading: "Content Loading",
|
||||||
|
contentLoadingDescription: "The requested page is currently unavailable or still loading.",
|
||||||
|
pageUrl: "Page URL",
|
||||||
|
language: "Language",
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
errorLoadingContent: "İçerik Yüklenirken Hata",
|
||||||
|
contentArea: "İçerik Alanı",
|
||||||
|
contentLoading: "İçerik Yükleniyor",
|
||||||
|
contentLoadingDescription: "İçerik Yüklenirken Hata",
|
||||||
|
pageUrl: "Sayfa URL",
|
||||||
|
language: "Dil",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FallbackContent: FC<{ lang: LanguageTypes; activePageUrl: string }> = memo(({ lang, activePageUrl }) => (
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{translations[lang].contentLoading}</h2>
|
||||||
|
<p className="text-gray-600 mb-4">{translations[lang].contentLoadingDescription}</p>
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
|
||||||
|
<p className="text-sm text-blue-700">{translations[lang].pageUrl}: {activePageUrl}</p>
|
||||||
|
<p className="text-sm text-blue-700">{translations[lang].language}: {lang}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
const ContentComponent: FC<ContentProps> = ({
|
||||||
|
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
|
||||||
|
onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
|
||||||
|
}) => {
|
||||||
|
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)]";
|
||||||
|
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||||
|
|
||||||
|
if (userLoading) { return <div className={classNameDiv}>{loadingContent}</div> }
|
||||||
|
if (userError) {
|
||||||
|
return (
|
||||||
|
<div className={classNameDiv}><div className="p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-red-600">{translations[lang].errorLoadingContent}</h2><p className="text-gray-600 mb-4">{userError}</p></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classNameDiv}>
|
||||||
|
<Suspense fallback={loadingContent}>
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">{translations[lang].contentArea}</h2>
|
||||||
|
{(!userData) && (<FallbackContent lang={lang} activePageUrl={activePageUrl || ''} />)}
|
||||||
|
<MemoizedMultiPage
|
||||||
|
activePageUrl={activePageUrl || ''} searchParams={searchParams}
|
||||||
|
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentComponent;
|
||||||
|
|
||||||
|
|
||||||
|
// {userData && (
|
||||||
|
// <div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||||
|
// {/* User Information */}
|
||||||
|
// <h3 className="text-lg font-semibold mb-2">User Information</h3>
|
||||||
|
// <p>User Type: {userData.user_tag || 'N/A'}</p>
|
||||||
|
// {userData.person && (
|
||||||
|
// <p>Name: {userData.person.firstname} {userData.person.surname}</p>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {selectionData && (
|
||||||
|
// <div className="mb-6 p-4 bg-green-50 rounded-lg">
|
||||||
|
// {/* Selection Information */}
|
||||||
|
// <h3 className="text-lg font-semibold mb-2">Selection Information</h3>
|
||||||
|
// <p>Current Page: {activePageUrl || 'Home'}</p>
|
||||||
|
// <p>Mode: {modeFromQuery}</p>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use client';
|
||||||
|
import { FC } from "react";
|
||||||
|
import { langGetKey } from "@/lib/langGet";
|
||||||
|
import { FooterProps } from "@/validations/mutual/dashboard/props";
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
footer: "footer",
|
||||||
|
page: "page"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
footer: "footer",
|
||||||
|
page: "sayfa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FooterComponent: FC<FooterProps> = ({
|
||||||
|
activePageUrl, configData, configLoading, configError,
|
||||||
|
onlineData, onlineLoading, onlineError
|
||||||
|
}) => {
|
||||||
|
// Use the config context hook
|
||||||
|
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{!configLoading && configData && (
|
||||||
|
<span>Theme: {configData.theme || 'Default'}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1>{langGetKey(translations[lang], "footer")}: {langGetKey(translations[lang], "page")}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{!configLoading && configData && (
|
||||||
|
<span>Text Size: {configData.textFont || 'Default'}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FooterComponent;
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from '@/components/mutual/ui/dropdown-menu';
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Navbar({ toggleSidebar }: NavbarProps) {
|
||||||
|
const [activeNotificationTab, setActiveNotificationTab] = useState('notifications');
|
||||||
|
|
||||||
|
// Toggle fullscreen
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(err => {
|
||||||
|
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||||
|
});
|
||||||
|
} else if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-2 px-6 bg-[#f8f4f3] flex items-center shadow-md shadow-black/5 sticky top-0 left-0 z-30">
|
||||||
|
<button type="button" className="text-lg text-gray-900 font-semibold sidebar-toggle md:hidden" onClick={toggleSidebar}>
|
||||||
|
<i className="ri-menu-line"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul className="ml-auto flex items-center">
|
||||||
|
{/* Search dropdown */}
|
||||||
|
<li className="mr-1">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||||
|
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-64">
|
||||||
|
<div className="p-4 border-b border-b-gray-100">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<input type="text" className="py-2 pr-4 pl-10 bg-gray-50 w-full outline-none border border-gray-100 rounded-md text-sm focus:border-blue-500" placeholder="Search..." />
|
||||||
|
<i className="ri-search-line absolute top-1/2 left-4 -translate-y-1/2 text-gray-900"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Notifications dropdown */}
|
||||||
|
<li>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||||
|
<path d="M19 13.586V10c0-3.217-2.185-5.927-5.145-6.742C13.562 2.52 12.846 2 12 2s-1.562.52-1.855 1.258C7.185 4.074 5 6.783 5 10v3.586l-1.707 1.707A.996.996 0 0 0 3 16v2a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L19 13.586zM19 17H5v-.586l1.707-1.707A.996.996 0 0 0 7 14v-4c0-2.757 2.243-5 5-5s5 2.243 5 5v4c0 .266.105.52.293.707L19 16.414V17zm-7 5a2.98 2.98 0 0 0 2.818-2H9.182A2.98 2.98 0 0 0 12 22z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-64">
|
||||||
|
<div className="flex items-center px-4 pt-4 border-b border-b-gray-100 notification-tab">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveNotificationTab('notifications')}
|
||||||
|
className={`text-gray-400 font-medium text-[13px] hover:text-gray-600 border-b-2 ${activeNotificationTab === 'notifications' ? 'border-b-blue-500 text-blue-500' : 'border-b-transparent'} mr-4 pb-1`}
|
||||||
|
>
|
||||||
|
Notifications
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveNotificationTab('messages')}
|
||||||
|
className={`text-gray-400 font-medium text-[13px] hover:text-gray-600 border-b-2 ${activeNotificationTab === 'messages' ? 'border-b-blue-500 text-blue-500' : 'border-b-transparent'} mr-4 pb-1`}
|
||||||
|
>
|
||||||
|
Messages
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-64 overflow-y-auto py-2">
|
||||||
|
{activeNotificationTab === 'notifications' ? (
|
||||||
|
<div>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<DropdownMenuItem key={`notif-${i}`} className="px-4 py-2 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div className="w-8 h-8 rounded bg-blue-500 flex items-center justify-center text-white font-medium">N</div>
|
||||||
|
<div className="ml-2 flex-grow">
|
||||||
|
<div className="text-[13px] text-gray-600 font-medium truncate">New order</div>
|
||||||
|
<div className="text-[11px] text-gray-400">from John Doe</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-gray-400">5 min ago</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<DropdownMenuItem key={`msg-${i}`} className="px-4 py-2 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div className="w-8 h-8 rounded bg-green-500 flex items-center justify-center text-white font-medium">J</div>
|
||||||
|
<div className="ml-2 flex-grow">
|
||||||
|
<div className="text-[13px] text-gray-600 font-medium truncate">John Doe</div>
|
||||||
|
<div className="text-[11px] text-gray-400">Hello there!</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-gray-400">5 min ago</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Fullscreen button */}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
className="text-gray-400 mr-4 w-8 h-8 rounded flex items-center justify-center hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className="hover:bg-gray-100 rounded-full" viewBox="0 0 24 24" style={{ fill: 'gray' }}>
|
||||||
|
<path d="M5 5h5V3H3v7h2zm5 14H5v-5H3v7h7zm11-5h-2v5h-5v2h7zm-2-4h2V3h-7v2h5z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Profile dropdown */}
|
||||||
|
<li>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="flex items-center">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-medium">
|
||||||
|
B
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem className="cursor-pointer">Profile</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="cursor-pointer">Settings</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem className="cursor-pointer">Logout</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
'use client';
|
||||||
|
import { FC } from "react";
|
||||||
|
import { HeaderProps } from "@/validations/mutual/dashboard/props";
|
||||||
|
import { langGetKey } from "@/lib/langGet";
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
import LanguageSelectionComponent from "@/components/mutual/languageSelection/component";
|
||||||
|
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
selectedPage: "selectedPage",
|
||||||
|
page: "page"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
selectedPage: "seçiliSayfa",
|
||||||
|
page: "sayfa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderComponent: FC<HeaderProps> = ({
|
||||||
|
activePageUrl, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
|
||||||
|
userData, userLoading, userError, refreshUser, updateUser
|
||||||
|
}) => {
|
||||||
|
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||||
|
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[lang], 'selectedPage')} :</p>
|
||||||
|
<p className="text-lg font-bold mx-3"> {activePageUrl || langGetKey(translations[lang], 'page')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{!onlineLoading && onlineData && onlineData.userType && (
|
||||||
|
<div className="mr-4 text-sm">
|
||||||
|
<span className="font-semibold">{lang}</span>
|
||||||
|
<span className="ml-2 text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">{onlineData.userType}</span>
|
||||||
|
</div>
|
||||||
|
)}<LanguageSelectionComponent
|
||||||
|
activePage={activePageUrl} onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderComponent;
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import renderOneClientSelection from "./renderOneClientSelection";
|
||||||
|
|
||||||
|
interface ClientSelectionSectionProps {
|
||||||
|
selectionData: any;
|
||||||
|
initialSelectedClient?: any;
|
||||||
|
onClientSelect?: (client: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientSelectionSection: FC<ClientSelectionSectionProps> = ({
|
||||||
|
selectionData,
|
||||||
|
initialSelectedClient = null,
|
||||||
|
onClientSelect
|
||||||
|
}) => {
|
||||||
|
const [selectedClient, setSelectedClient] = useState<any>(initialSelectedClient);
|
||||||
|
if (!selectionData || !selectionData.selectionList || selectionData.selectionList.length === 0) { return null }
|
||||||
|
const handleClientSelection = (client: any) => { setSelectedClient(client); if (onClientSelect) { onClientSelect(client) } };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
{selectionData.selectionList.map((client: any, index: number) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={client.uu_id || client.id || `client-${index}`}
|
||||||
|
onClick={() => handleClientSelection(client)}
|
||||||
|
>
|
||||||
|
{client && renderOneClientSelection({
|
||||||
|
item: client,
|
||||||
|
isSelected: selectedClient?.uu_id === client.uu_id,
|
||||||
|
onClickHandler: handleClientSelection
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientSelectionSection;
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use client';
|
||||||
|
import { FC, Suspense } from "react";
|
||||||
|
import { MenuProps } from "@/validations/mutual/dashboard/props";
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
|
||||||
|
import UserProfileSection from "./userProfileSection";
|
||||||
|
import ClientSelectionSection from "./clientSelectionSection";
|
||||||
|
import MenuItemsSection from "./menuItemsSection";
|
||||||
|
import MenuLoadingState from "./menuLoadingState";
|
||||||
|
import MenuErrorState from "./menuErrorState";
|
||||||
|
import MenuEmptyState from "./menuEmptyState";
|
||||||
|
import LoadingContent from "@/components/mutual/loader/component";
|
||||||
|
|
||||||
|
const MenuComponent: FC<MenuProps> = ({
|
||||||
|
activePageUrl, availableApplications, prefix,
|
||||||
|
onlineData, onlineLoading, onlineError,
|
||||||
|
userData, userLoading, userError,
|
||||||
|
selectionData, selectionLoading, selectionError,
|
||||||
|
menuData, menuLoading, menuError
|
||||||
|
}) => {
|
||||||
|
if (menuLoading) { return <MenuLoadingState /> } // Render loading state
|
||||||
|
if (menuError) { return <MenuErrorState error={menuError} />; } // Render error state
|
||||||
|
if (availableApplications.length === 0) { return <MenuEmptyState />; } // Render empty state
|
||||||
|
function handleClientSelection(client: any) { console.log('Client selected:', client) }
|
||||||
|
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||||
|
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">
|
||||||
|
{/* User Profile Section */}
|
||||||
|
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-content"} plane="h-full w-full" /></div>}>
|
||||||
|
<UserProfileSection userData={userData} onlineData={onlineData} />
|
||||||
|
</Suspense>
|
||||||
|
{/* Client Selection Section */}
|
||||||
|
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-content"} plane="h-full w-full" /></div>}>
|
||||||
|
<ClientSelectionSection selectionData={selectionData} initialSelectedClient={selectionData} onClientSelect={handleClientSelection} />
|
||||||
|
</Suspense>
|
||||||
|
{/* Menu Items Section */}
|
||||||
|
<Suspense fallback={<div><LoadingContent height="h-16" size="w-36 h-48" key={"loading-content"} plane="h-full w-full" /></div>}>
|
||||||
|
<MenuItemsSection availableApplications={availableApplications} activePageUrl={activePageUrl} lang={lang} prefix={prefix} />
|
||||||
|
</Suspense>
|
||||||
|
</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,18 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const MenuEmptyState: FC = () => {
|
||||||
|
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 justify-center items-center h-full">
|
||||||
|
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||||
|
<p className="text-sm">No menu items available</p>
|
||||||
|
<p className="text-xs mt-1">Please check your permissions or contact an administrator</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuEmptyState;
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
interface MenuErrorStateProps {
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuErrorState: FC<MenuErrorStateProps> = ({ error }) => {
|
||||||
|
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 justify-center items-center h-full">
|
||||||
|
<div className="bg-red-100 dark:bg-red-900 border border-red-400 text-red-700 dark:text-red-200 px-3 py-2 rounded relative text-xs" role="alert">
|
||||||
|
<strong className="font-bold">Error loading menu: </strong>
|
||||||
|
<span className="block sm:inline">{error}</span>
|
||||||
|
<button
|
||||||
|
className="mt-2 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded text-xs"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuErrorState;
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
'use client';
|
||||||
|
import React, { FC, useEffect, useState } from "react";
|
||||||
|
import { menuTranslation } from "@/languages/mutual/menu";
|
||||||
|
import FirstLayerDropdown from "./firstLayerComponent";
|
||||||
|
import SecondLayerDropdown from "./secondLayerComponent";
|
||||||
|
import ThirdLayerDropdown from "./thirdLayerComponent";
|
||||||
|
|
||||||
|
type TranslationItem = { value: string; key: string };
|
||||||
|
type ThirdLayerItemData = { path: string; translation: TranslationItem[] };
|
||||||
|
type ThirdLayerItem = Record<string, ThirdLayerItemData>;
|
||||||
|
type SecondLayerItems = Record<string, ThirdLayerItem>;
|
||||||
|
type FirstLayerItems = Record<string, SecondLayerItems>;
|
||||||
|
type MenuStructure = FirstLayerItems;
|
||||||
|
|
||||||
|
interface MenuItemsSectionProps {
|
||||||
|
availableApplications: string[];
|
||||||
|
activePageUrl: string;
|
||||||
|
lang: string;
|
||||||
|
prefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuStaticTranslation = {
|
||||||
|
tr: { menu: "Menü" },
|
||||||
|
en: { menu: "Menu" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuItemsSection: FC<MenuItemsSectionProps> = ({ availableApplications, activePageUrl, lang, prefix }) => {
|
||||||
|
const [expandedFirstLayer, setExpandedFirstLayer] = useState<string | null>(null);
|
||||||
|
const [expandedSecondLayer, setExpandedSecondLayer] = useState<string | null>(null);
|
||||||
|
const [menuStructure, setMenuStructure] = useState<MenuStructure>({});
|
||||||
|
|
||||||
|
const menuTranslationWLang = menuTranslation[lang as keyof typeof menuTranslation];
|
||||||
|
const activeParsedLayer = (menuTranslationWLang[activePageUrl as keyof typeof menuTranslationWLang] as unknown as TranslationItem[]) || [];
|
||||||
|
const activeFirstLayer = activeParsedLayer[0] ? activeParsedLayer[0].key : null;
|
||||||
|
const activeSecondLayer = activeParsedLayer[1] ? activeParsedLayer[1].key : null;
|
||||||
|
const activeThirdLayer = activeParsedLayer[2] ? activeParsedLayer[2].key : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newMenuStructure: MenuStructure = {};
|
||||||
|
availableApplications.forEach((appPath: string) => {
|
||||||
|
const pathTranslation = menuTranslationWLang[appPath as keyof typeof menuTranslationWLang] as unknown as TranslationItem[] | undefined;
|
||||||
|
if (pathTranslation && pathTranslation.length >= 3) {
|
||||||
|
const firstLayer = pathTranslation[0] ? pathTranslation[0].key : '';
|
||||||
|
const secondLayer = pathTranslation[1] ? pathTranslation[1].key : '';
|
||||||
|
const thirdLayer = pathTranslation[2] ? pathTranslation[2].key : '';
|
||||||
|
if (!newMenuStructure[firstLayer]) { newMenuStructure[firstLayer] = {} }
|
||||||
|
if (!newMenuStructure[firstLayer][secondLayer]) { newMenuStructure[firstLayer][secondLayer] = {} }
|
||||||
|
newMenuStructure[firstLayer][secondLayer][thirdLayer] = { path: appPath, translation: pathTranslation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setMenuStructure(newMenuStructure);
|
||||||
|
}, [availableApplications, menuTranslationWLang]);
|
||||||
|
|
||||||
|
useEffect(() => { if (activeFirstLayer) { setExpandedFirstLayer(activeFirstLayer); if (activeSecondLayer) { setExpandedSecondLayer(activeSecondLayer) } } }, [activeFirstLayer, activeSecondLayer]);
|
||||||
|
|
||||||
|
const handleFirstLayerClick = (key: string) => { if (expandedFirstLayer === key) { setExpandedFirstLayer(null); setExpandedSecondLayer(null) } else { setExpandedFirstLayer(key); setExpandedSecondLayer(null) } };
|
||||||
|
const handleSecondLayerClick = (key: string) => { if (expandedSecondLayer === key) { setExpandedSecondLayer(null) } else { setExpandedSecondLayer(key) } };
|
||||||
|
const renderThirdLayerItems = (firstLayerKey: string, secondLayerKey: string, thirdLayerItems: ThirdLayerItem) => {
|
||||||
|
return Object.entries(thirdLayerItems).map(([thirdLayerKey, itemData]) => {
|
||||||
|
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey && activeThirdLayer === thirdLayerKey;
|
||||||
|
const url = itemData ? itemData.path || '' : '';
|
||||||
|
const translation = itemData ? itemData.translation || [] : [];
|
||||||
|
const displayText = translation[2]?.value || thirdLayerKey;
|
||||||
|
return <div key={`${thirdLayerKey}-item`} className="ml-2 my-1"><ThirdLayerDropdown isActive={isActive} innerText={displayText} url={`${prefix}${url}`} /></div>;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const renderSecondLayerItems = (firstLayerKey: string, secondLayerItems: SecondLayerItems) => {
|
||||||
|
return Object.entries(secondLayerItems).map(([secondLayerKey, thirdLayerItems]) => {
|
||||||
|
const isActive = activeFirstLayer === firstLayerKey && activeSecondLayer === secondLayerKey;
|
||||||
|
const isExpanded = expandedSecondLayer === secondLayerKey;
|
||||||
|
const anyThirdLayerItem = Object.values(thirdLayerItems)[0];
|
||||||
|
const translation = anyThirdLayerItem ? anyThirdLayerItem.translation : [];
|
||||||
|
const displayText = translation[1]?.value || secondLayerKey;
|
||||||
|
return (
|
||||||
|
<div key={`${secondLayerKey}-item`} className="ml-2 my-1">
|
||||||
|
<SecondLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={displayText} onClick={() => handleSecondLayerClick(secondLayerKey)} />
|
||||||
|
{isExpanded && <div className="ml-2 mt-1">{renderThirdLayerItems(firstLayerKey, secondLayerKey, thirdLayerItems)}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const renderFirstLayerItems = () => {
|
||||||
|
return Object.entries(menuStructure).map(([firstLayerKey, secondLayerItems]) => {
|
||||||
|
const isActive = activeFirstLayer === firstLayerKey;
|
||||||
|
const isExpanded = expandedFirstLayer === firstLayerKey;
|
||||||
|
const anySecondLayer = Object.values(secondLayerItems)[0];
|
||||||
|
const anyThirdLayerItem = anySecondLayer ? Object.values(anySecondLayer)[0] : null;
|
||||||
|
const translation = anyThirdLayerItem ? anyThirdLayerItem.translation : [];
|
||||||
|
const displayText = translation[0]?.value || firstLayerKey;
|
||||||
|
return (
|
||||||
|
<div key={`${firstLayerKey}-item`} className="mb-2">
|
||||||
|
<FirstLayerDropdown isActive={isActive} isExpanded={isExpanded} innerText={displayText} onClick={() => handleFirstLayerClick(firstLayerKey)} />
|
||||||
|
{isExpanded && <div className="mt-1">{renderSecondLayerItems(firstLayerKey, secondLayerItems)}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return <div className="mt-1"><h3 className="text-sm font-semibold mb-1">{menuStaticTranslation[lang as keyof typeof menuStaticTranslation].menu}</h3>{renderFirstLayerItems()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuItemsSection;
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const MenuLoadingState: FC = () => {
|
||||||
|
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 justify-center items-center h-full">
|
||||||
|
<div className="animate-pulse flex flex-col space-y-2 w-full">
|
||||||
|
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
|
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-2/3"></div>
|
||||||
|
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
|
||||||
|
<div className="text-center text-xs text-gray-500 dark:text-gray-400 mt-2">Loading menu...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuLoadingState;
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use client';
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Briefcase } from "lucide-react";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: any;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClickHandler: (item: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderOneClientSelection: FC<Props> = ({ item, isSelected, onClickHandler }) => {
|
||||||
|
if (isSelected) {
|
||||||
|
return (
|
||||||
|
<div key={item.uu_id} onClick={() => { onClickHandler(item) }}
|
||||||
|
className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md mb-2 cursor-pointer">
|
||||||
|
<div className="bg-amber-300 p-2 hover:bg-amber-400 transition-all">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2 relative">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-amber-400 flex items-center justify-center overflow-hidden border border-white">
|
||||||
|
{item.avatar ? (<img src={item.avatar} alt="Company" className="w-full h-full object-cover" />) :
|
||||||
|
(<div className="text-white text-xs font-bold">{(item.public_name || "No Name").slice(0, 2)}</div>)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 bg-white p-0.5 rounded-full border border-amber-400">
|
||||||
|
<Briefcase size={8} className="text-amber-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<h2 className="text-xs font-bold text-black truncate">{item.public_name} {item.company_type}</h2>
|
||||||
|
<p className="text-xs text-amber-800 truncate">{item.duty}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={item.uu_id} className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all mb-2 cursor-pointer">
|
||||||
|
<div className="bg-gray-100 p-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2 relative">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center overflow-hidden border border-white">
|
||||||
|
{item.avatar ? (<img src={item.avatar} alt="Company" className="w-full h-full object-cover" />) :
|
||||||
|
(<div className="text-white text-xs font-bold">{(item.duty || "No Duty").slice(0, 2)}</div>)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 bg-white p-0.5 rounded-full border border-gray-300">
|
||||||
|
<Briefcase size={8} className="text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<h2 className="text-xs font-bold text-gray-700 truncate">{item.public_name} {item.company_type}</h2>
|
||||||
|
<p className="text-xs text-gray-600 truncate">{item.duty}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenderOneClientSelection;
|
||||||
|
|
@ -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,36 @@
|
||||||
|
|
||||||
|
import { ClientUser } from "@/types/mutual/context/validations";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userProfile: ClientUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfile: FC<Props> = ({ userProfile }) => {
|
||||||
|
if (!userProfile || !userProfile.person) return;
|
||||||
|
const profileuser: ClientUser = JSON.parse(JSON.stringify(userProfile));
|
||||||
|
return (
|
||||||
|
<div className="max-w-md w-full text-md bg-white shadow-lg rounded-lg overflow-hidden transition-all hover:shadow-xl">
|
||||||
|
<div className="bg-amber-300 p-4 hover:bg-amber-400 transition-all">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-4">
|
||||||
|
<img className="rounded-full border-2 border-white" src={profileuser.avatar} alt="Avatar" width={80} height={80} />
|
||||||
|
</div>
|
||||||
|
<div><h2 className="text-md font-bold text-black">Profile Info</h2></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-2 flex items-center">
|
||||||
|
<span className="font-semibold w-28 text-gray-700">Email:</span>
|
||||||
|
<span className="text-gray-800">{profileuser.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2 flex items-center">
|
||||||
|
<span className="font-semibold w-28 text-gray-700">Full Name:</span>
|
||||||
|
<span className="text-gray-800">{profileuser.person.firstname} {profileuser.person.surname}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserProfile;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { ClientUser } from "@/types/mutual/context/validations";
|
||||||
|
import { ClientOnline } from "@/types/mutual/context/validations";
|
||||||
|
|
||||||
|
interface UserProfileSectionProps {
|
||||||
|
userData: ClientUser | null;
|
||||||
|
onlineData: ClientOnline | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserProfileSection: FC<UserProfileSectionProps> = ({ userData, onlineData }) => {
|
||||||
|
if (!userData) return null;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="w-full text-xs bg-white shadow rounded-lg overflow-hidden transition-all hover:shadow-md">
|
||||||
|
<div className="bg-amber-300 p-2 hover:bg-amber-400 transition-all">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2">
|
||||||
|
{userData && userData.avatar ? (<img className="rounded-full border border-white" src={userData.avatar} alt="Avatar" width={40} height={40} />) : (
|
||||||
|
<div className="w-10 h-10 rounded-full bg-amber-400 flex items-center justify-center border border-white">
|
||||||
|
<div className="text-white text-sm font-bold">{userData?.email ? userData.email.slice(0, 2).toUpperCase() : 'U'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<h2 className="text-sm font-bold text-black truncate">{userData?.person ? `${userData.person.firstname} ${userData.person.surname}` : 'User'}</h2>
|
||||||
|
<p className="text-xs text-amber-800 truncate">{userData?.email || 'No email'}</p>
|
||||||
|
<p className="text-xs font-medium capitalize">{onlineData?.userType || 'guest'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfileSection;
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface RenderOneClientSelectionProps {
|
||||||
|
item: any;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClickHandler: (client: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderOneClientSelection = ({
|
||||||
|
item,
|
||||||
|
isSelected,
|
||||||
|
onClickHandler
|
||||||
|
}: RenderOneClientSelectionProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full text-xs bg-white shadow rounded-lg overflow-hidden mb-2 transition-all ${isSelected ? 'border-2 border-amber-500' : ''} hover:shadow-md`}
|
||||||
|
onClick={() => onClickHandler(item)}
|
||||||
|
>
|
||||||
|
<div className={`${isSelected ? 'bg-amber-100' : 'bg-gray-50'} p-2 hover:bg-amber-50 transition-all`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="mr-2">
|
||||||
|
{item.logo ? (
|
||||||
|
<img className="rounded border border-gray-200" src={item.logo} alt="Logo" width={40} height={40} />
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 rounded bg-gray-200 flex items-center justify-center border border-gray-300">
|
||||||
|
<div className="text-gray-500 text-sm font-bold">{item.name ? item.name.slice(0, 2).toUpperCase() : 'C'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<h2 className="text-sm font-bold text-gray-800 truncate">{item.name || 'Client'}</h2>
|
||||||
|
<p className="text-xs text-gray-500 truncate">{item.description || 'No description'}</p>
|
||||||
|
</div>
|
||||||
|
{isSelected && (
|
||||||
|
<div className="ml-auto">
|
||||||
|
<div className="bg-amber-500 rounded-full w-4 h-4 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default renderOneClientSelection;
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ClientPageConfig } from "@/types/mutual/context";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { createContextHook } from "../hookFactory";
|
||||||
|
|
||||||
|
// Original fetch functions for backward compatibility
|
||||||
|
async function checkContextPageConfig(): Promise<ClientPageConfig | null> {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/config`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
if (data.status === 200) return data.data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking page config:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setContextPageConfig({
|
||||||
|
pageConfig,
|
||||||
|
}: {
|
||||||
|
pageConfig: ClientPageConfig;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/config`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(pageConfig),
|
||||||
|
});
|
||||||
|
const data = await result.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting page config:", error);
|
||||||
|
throw new Error("No data is set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the config hook using the factory
|
||||||
|
const useContextConfig = createContextHook<ClientPageConfig>({
|
||||||
|
endpoint: "/context/page/config",
|
||||||
|
contextName: "config",
|
||||||
|
enablePeriodicRefresh: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook for config data with the expected interface
|
||||||
|
interface UseConfigResult {
|
||||||
|
configData: ClientPageConfig | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refreshConfig: () => Promise<void>;
|
||||||
|
updateConfig: (newConfig: ClientPageConfig) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper hook that adapts the generic hook to the expected interface
|
||||||
|
export function useConfig(): UseConfigResult {
|
||||||
|
const { data, isLoading, error, refresh, update } = useContextConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
configData: data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refreshConfig: refresh,
|
||||||
|
updateConfig: update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkContextPageConfig, setContextPageConfig };
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
|
||||||
|
// Default timeout for fetch requests
|
||||||
|
const FETCH_TIMEOUT = 5000; // 5 seconds
|
||||||
|
|
||||||
|
interface UseContextHookOptions<T> {
|
||||||
|
// The endpoint path (without API_BASE_URL)
|
||||||
|
endpoint: string;
|
||||||
|
|
||||||
|
// The name of the context for logging
|
||||||
|
contextName: string;
|
||||||
|
|
||||||
|
// Function to extract available items (e.g., selectionList for menu)
|
||||||
|
extractAvailableItems?: (data: T) => string[];
|
||||||
|
|
||||||
|
// Whether to enable periodic refresh
|
||||||
|
enablePeriodicRefresh?: boolean;
|
||||||
|
|
||||||
|
// Refresh interval in milliseconds (default: 5 minutes)
|
||||||
|
refreshInterval?: number;
|
||||||
|
|
||||||
|
// Custom fetch function for getting data
|
||||||
|
customFetch?: () => Promise<T | null>;
|
||||||
|
|
||||||
|
// Custom update function for setting data
|
||||||
|
customUpdate?: (newData: T) => Promise<boolean>;
|
||||||
|
|
||||||
|
// Default value to use when data is not available
|
||||||
|
defaultValue?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseContextHookResult<T> {
|
||||||
|
data: T | null;
|
||||||
|
availableItems: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
update: (newData: T) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a custom hook for any context type
|
||||||
|
*/
|
||||||
|
export function createContextHook<T>(options: UseContextHookOptions<T>) {
|
||||||
|
const {
|
||||||
|
endpoint,
|
||||||
|
contextName,
|
||||||
|
extractAvailableItems = () => [],
|
||||||
|
enablePeriodicRefresh = false,
|
||||||
|
refreshInterval = 5 * 60 * 1000, // 5 minutes
|
||||||
|
customFetch,
|
||||||
|
customUpdate,
|
||||||
|
defaultValue,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// The API endpoint paths
|
||||||
|
const apiEndpoint = `/api${endpoint}`;
|
||||||
|
const directEndpoint = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data from the context API
|
||||||
|
*/
|
||||||
|
async function fetchData(): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
// Create an AbortController to handle timeouts
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
const result = await fetch(directEndpoint, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
|
if (data.status === 200 && data.data) {
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates data in the context API
|
||||||
|
*/
|
||||||
|
async function updateData(newData: T): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Create an AbortController to handle timeouts
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
const result = await fetch(directEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newData),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
const data = await result.json();
|
||||||
|
return data.status === 200;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the custom hook
|
||||||
|
return function useContextHook(): UseContextHookResult<T> {
|
||||||
|
const [data, setData] = useState<T | null>(defaultValue || null);
|
||||||
|
const [availableItems, setAvailableItems] = useState<string[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [selectedClient, setSelectedClient] = useState<any>(null);
|
||||||
|
|
||||||
|
const refreshData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
if (customFetch) {
|
||||||
|
try {
|
||||||
|
const customData = await customFetch();
|
||||||
|
|
||||||
|
if (customData) {
|
||||||
|
setData(customData);
|
||||||
|
|
||||||
|
if (extractAvailableItems) {
|
||||||
|
const items = extractAvailableItems(customData);
|
||||||
|
setAvailableItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (customError) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiEndpoint);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch ${contextName} data: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === 200 && result.data) {
|
||||||
|
setData(result.data);
|
||||||
|
|
||||||
|
if (extractAvailableItems) {
|
||||||
|
const items = extractAvailableItems(result.data);
|
||||||
|
setAvailableItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn(
|
||||||
|
`API endpoint failed, falling back to direct fetch:`,
|
||||||
|
apiError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const directData = await fetchData();
|
||||||
|
if (directData) {
|
||||||
|
setData(directData);
|
||||||
|
if (extractAvailableItems) {
|
||||||
|
const items = extractAvailableItems(directData);
|
||||||
|
setAvailableItems(items);
|
||||||
|
}
|
||||||
|
} else if (defaultValue) {
|
||||||
|
setData(defaultValue);
|
||||||
|
|
||||||
|
if (extractAvailableItems) {
|
||||||
|
const items = extractAvailableItems(defaultValue);
|
||||||
|
setAvailableItems(items);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setData(null);
|
||||||
|
setAvailableItems([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
if (defaultValue) {
|
||||||
|
setData(defaultValue);
|
||||||
|
if (extractAvailableItems) {
|
||||||
|
const items = extractAvailableItems(defaultValue);
|
||||||
|
setAvailableItems(items);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setData(null);
|
||||||
|
setAvailableItems([]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to update data
|
||||||
|
const update = useCallback(
|
||||||
|
async (newData: T): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (customUpdate) {
|
||||||
|
try {
|
||||||
|
const success = await customUpdate(newData);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await refreshData();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
setError("Failed to update data with custom update function");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (customError) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to update ${contextName} data: ${response.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === 200) {
|
||||||
|
await refreshData(); // Refresh data after update
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (apiError) {
|
||||||
|
console.warn(
|
||||||
|
`API update failed, falling back to direct update:`,
|
||||||
|
apiError
|
||||||
|
);
|
||||||
|
// Fall back to direct update if API update fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to direct update
|
||||||
|
const success = await updateData(newData);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Refresh data to get the updated state
|
||||||
|
await refreshData();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
setError("Failed to update data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error updating ${contextName} data:`, err);
|
||||||
|
setError(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[refreshData]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch data on component mount and set up periodic refresh if enabled
|
||||||
|
useEffect(() => {
|
||||||
|
refreshData();
|
||||||
|
|
||||||
|
// Set up periodic refresh if enabled
|
||||||
|
if (enablePeriodicRefresh) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
refreshData();
|
||||||
|
}, refreshInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [refreshData, enablePeriodicRefresh, refreshInterval]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
availableItems,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refresh: refreshData,
|
||||||
|
update: update,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ClientMenu } from "@/types/mutual/context";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { createContextHook } from "../hookFactory";
|
||||||
|
|
||||||
|
// Original fetch functions for backward compatibility
|
||||||
|
async function checkContextPageMenu() {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const data = await result.json();
|
||||||
|
if (data.status == 200) return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("No data is found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setContextPageMenu(setMenu: ClientMenu) {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/menu`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(setMenu),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const data = await result.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("No data is set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the menu hook using the factory
|
||||||
|
const useContextMenu = createContextHook<ClientMenu>({
|
||||||
|
endpoint: "/context/page/menu",
|
||||||
|
contextName: "menu",
|
||||||
|
extractAvailableItems: (data) => data.selectionList || [],
|
||||||
|
enablePeriodicRefresh: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook for menu data with the expected interface
|
||||||
|
interface UseMenuResult {
|
||||||
|
menuData: ClientMenu | null;
|
||||||
|
availableApplications: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refreshMenu: () => Promise<void>;
|
||||||
|
updateMenu: (newMenu: ClientMenu) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper hook that adapts the generic hook to the expected interface
|
||||||
|
export function useMenu(): UseMenuResult {
|
||||||
|
const { data, availableItems, isLoading, error, refresh, update } =
|
||||||
|
useContextMenu();
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuData: data,
|
||||||
|
availableApplications: availableItems,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refreshMenu: refresh,
|
||||||
|
updateMenu: update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkContextPageMenu, setContextPageMenu };
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ClientOnline } from "@/types/mutual/context";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { createContextHook } from "../hookFactory";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const FETCH_TIMEOUT = 5000; // 5 seconds timeout
|
||||||
|
|
||||||
|
// Default online state to use when API calls fail
|
||||||
|
const DEFAULT_ONLINE_STATE: ClientOnline = {
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastLogout: new Date(),
|
||||||
|
lastAction: new Date(),
|
||||||
|
lastPage: "/",
|
||||||
|
userType: "guest",
|
||||||
|
lang: "en",
|
||||||
|
timezone: "UTC",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current online state from the context API
|
||||||
|
* @returns The online state or null if there was an error
|
||||||
|
*/
|
||||||
|
async function checkContextPageOnline(): Promise<ClientOnline | null> {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/online`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
|
if (data.status === 200 && data.data) {
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (fetchError) {
|
||||||
|
// Clear the timeout if it hasn't fired yet
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
fetchError instanceof DOMException &&
|
||||||
|
fetchError.name === "AbortError"
|
||||||
|
) {
|
||||||
|
return DEFAULT_ONLINE_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw fetchError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
return DEFAULT_ONLINE_STATE;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Error fetching online state:",
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
);
|
||||||
|
// Return default state instead of null for better user experience
|
||||||
|
return DEFAULT_ONLINE_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the online state in the context API
|
||||||
|
* @param setOnline The new online state to set
|
||||||
|
* @returns The updated online state or null if there was an error
|
||||||
|
*/
|
||||||
|
async function setContextPageOnline(
|
||||||
|
setOnline: ClientOnline
|
||||||
|
): Promise<ClientOnline | null> {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/page/online`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(setOnline),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
console.log("result", await result.json());
|
||||||
|
|
||||||
|
// Clear the timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
|
if (data.status === 200 && data.data) {
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the online hook using the factory
|
||||||
|
const useContextOnline = createContextHook<ClientOnline>({
|
||||||
|
endpoint: "/context/page/online",
|
||||||
|
contextName: "online",
|
||||||
|
enablePeriodicRefresh: true,
|
||||||
|
refreshInterval: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook for online data with the expected interface
|
||||||
|
interface UseOnlineResult {
|
||||||
|
onlineData: ClientOnline | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refreshOnline: () => Promise<void>;
|
||||||
|
updateOnline: (newOnline: ClientOnline) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper hook that adapts the generic hook to the expected interface
|
||||||
|
export function useOnline(): UseOnlineResult {
|
||||||
|
const { data, isLoading, error, refresh, update } = useContextOnline();
|
||||||
|
return {
|
||||||
|
onlineData: data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refreshOnline: refresh,
|
||||||
|
updateOnline: update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkContextPageOnline, setContextPageOnline };
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { FC, ReactNode, createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||||
|
import { checkContextPageOnline, setContextPageOnline } from './context';
|
||||||
|
import { setOnlineToRedis } from '@/fetchers/custom/context/page/online/fetch';
|
||||||
|
|
||||||
|
interface ClientOnline {
|
||||||
|
lang: string;
|
||||||
|
userType: string;
|
||||||
|
lastLogin: Date;
|
||||||
|
lastLogout: Date;
|
||||||
|
lastAction: Date;
|
||||||
|
lastPage: string;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default online state to use as fallback
|
||||||
|
const DEFAULT_ONLINE_STATE: ClientOnline = {
|
||||||
|
lang: "en",
|
||||||
|
userType: "occupant",
|
||||||
|
lastLogin: new Date(),
|
||||||
|
lastLogout: new Date(),
|
||||||
|
lastAction: new Date(),
|
||||||
|
lastPage: "/auth/login",
|
||||||
|
timezone: "GMT+3"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create context with default values
|
||||||
|
interface OnlineContextType {
|
||||||
|
online: ClientOnline | null;
|
||||||
|
updateOnline: (newOnline: ClientOnline) => Promise<boolean>;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
retryFetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnlineContext = createContext<OnlineContextType>({
|
||||||
|
online: null,
|
||||||
|
updateOnline: async () => false,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
retryFetch: async () => { }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook to use the context
|
||||||
|
export const useOnline = () => useContext(OnlineContext);
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
interface OnlineProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OnlineProvider: FC<OnlineProviderProps> = ({ children }) => {
|
||||||
|
const [online, setOnline] = useState<ClientOnline | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [retryCount, setRetryCount] = useState<number>(0);
|
||||||
|
const [lastRetryTime, setLastRetryTime] = useState<number>(0);
|
||||||
|
|
||||||
|
// Maximum number of automatic retries
|
||||||
|
const MAX_AUTO_RETRIES = 3;
|
||||||
|
// Minimum time between retries in milliseconds (5 seconds)
|
||||||
|
const MIN_RETRY_INTERVAL = 5000;
|
||||||
|
|
||||||
|
// Function to fetch online state
|
||||||
|
const fetchOnline = useCallback(async (force = false) => {
|
||||||
|
// Don't fetch if we already have data and it's not forced
|
||||||
|
if (online && !force && !error) {
|
||||||
|
console.log("Using existing online state:", online);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't retry too frequently
|
||||||
|
const now = Date.now();
|
||||||
|
if (!force && now - lastRetryTime < MIN_RETRY_INTERVAL) {
|
||||||
|
console.log("Retry attempted too soon, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setLastRetryTime(now);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Fetching online state...");
|
||||||
|
const data = await checkContextPageOnline();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
console.log("Successfully fetched online state:", data);
|
||||||
|
setOnline(data);
|
||||||
|
setRetryCount(0); // Reset retry count on success
|
||||||
|
} else {
|
||||||
|
console.warn("No online state returned, using default");
|
||||||
|
setOnline(DEFAULT_ONLINE_STATE);
|
||||||
|
setError("Could not retrieve online state, using default values");
|
||||||
|
|
||||||
|
// Auto-retry if under the limit
|
||||||
|
if (retryCount < MAX_AUTO_RETRIES) {
|
||||||
|
setRetryCount(prev => prev + 1);
|
||||||
|
console.log(`Scheduling retry ${retryCount + 1}/${MAX_AUTO_RETRIES}...`);
|
||||||
|
setTimeout(() => fetchOnline(true), MIN_RETRY_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error("Error fetching online state:", errorMessage);
|
||||||
|
setError(`Failed to fetch online state: ${errorMessage}`);
|
||||||
|
setOnline(DEFAULT_ONLINE_STATE); // Use default as fallback
|
||||||
|
|
||||||
|
// Auto-retry if under the limit
|
||||||
|
if (retryCount < MAX_AUTO_RETRIES) {
|
||||||
|
setRetryCount(prev => prev + 1);
|
||||||
|
console.log(`Scheduling retry ${retryCount + 1}/${MAX_AUTO_RETRIES}...`);
|
||||||
|
setTimeout(() => fetchOnline(true), MIN_RETRY_INTERVAL);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [online, error, retryCount, lastRetryTime]);
|
||||||
|
|
||||||
|
// Manual retry function that can be called from components
|
||||||
|
const retryFetch = useCallback(async () => {
|
||||||
|
console.log("Manual retry requested");
|
||||||
|
setRetryCount(0); // Reset retry count for manual retry
|
||||||
|
await fetchOnline(true);
|
||||||
|
}, [fetchOnline]);
|
||||||
|
|
||||||
|
// Fetch online state on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("OnlineProvider mounted, fetching initial data");
|
||||||
|
|
||||||
|
// Always fetch data on mount
|
||||||
|
fetchOnline();
|
||||||
|
|
||||||
|
// Set up periodic refresh (every 5 minutes)
|
||||||
|
const refreshInterval = setInterval(() => {
|
||||||
|
console.log("Performing periodic refresh of online state");
|
||||||
|
fetchOnline(true);
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("OnlineProvider unmounted, clearing interval");
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
};
|
||||||
|
}, [fetchOnline]);
|
||||||
|
|
||||||
|
// Function to update online state
|
||||||
|
const updateOnline = async (newOnline: ClientOnline): Promise<boolean> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Updating online state:", newOnline);
|
||||||
|
// Update Redis first
|
||||||
|
console.log('Updating Redis...');
|
||||||
|
await setOnlineToRedis(newOnline);
|
||||||
|
|
||||||
|
// Then update context API
|
||||||
|
console.log('Updating context API...');
|
||||||
|
await setContextPageOnline(newOnline);
|
||||||
|
|
||||||
|
// Finally update local state to trigger re-renders
|
||||||
|
console.log('Updating local state...');
|
||||||
|
setOnline(newOnline);
|
||||||
|
|
||||||
|
console.log('Online state updated successfully');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating online state:', error);
|
||||||
|
|
||||||
|
// Still update local state to maintain UI consistency
|
||||||
|
// even if the backend updates failed
|
||||||
|
setOnline(newOnline);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add debug logging for provider state
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('OnlineProvider state updated:', {
|
||||||
|
online: online ? 'present' : 'not present',
|
||||||
|
isLoading
|
||||||
|
});
|
||||||
|
}, [online, isLoading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnlineContext.Provider value={{ online, updateOnline, isLoading, error, retryFetch }}>
|
||||||
|
{children}
|
||||||
|
</OnlineContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export as default for backward compatibility
|
||||||
|
export default OnlineProvider;
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ClientSelection } from "@/types/mutual/context";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { createContextHook } from '../hookFactory';
|
||||||
|
|
||||||
|
// Original fetch functions for backward compatibility
|
||||||
|
async function checkContextDashSelection() {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/dash/selection`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
if (data.status === 200) return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking dash selection:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setContextDashUserSelection({
|
||||||
|
userSet,
|
||||||
|
}: {
|
||||||
|
userSet: ClientSelection;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/dash/selection`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(userSet),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
if (data.status === 200) return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting dash selection:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the selection hook using the factory
|
||||||
|
const useContextSelection = createContextHook<ClientSelection>({
|
||||||
|
endpoint: '/context/dash/selection',
|
||||||
|
contextName: 'selection',
|
||||||
|
enablePeriodicRefresh: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook for selection data with the expected interface
|
||||||
|
interface UseSelectionResult {
|
||||||
|
selectionData: ClientSelection | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refreshSelection: () => Promise<void>;
|
||||||
|
updateSelection: (newSelection: ClientSelection) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper hook that adapts the generic hook to the expected interface
|
||||||
|
export function useSelection(): UseSelectionResult {
|
||||||
|
const { data, isLoading, error, refresh, update } = useContextSelection();
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionData: data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refreshSelection: refresh,
|
||||||
|
updateSelection: update
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkContextDashSelection, setContextDashUserSelection };
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ClientUser } from "@/types/mutual/context";
|
||||||
|
import { API_BASE_URL } from "@/config/config";
|
||||||
|
import { createContextHook } from "../hookFactory";
|
||||||
|
import {
|
||||||
|
getUserFromRedis as getUserFromServer,
|
||||||
|
setUserToRedis as setUserFromServer,
|
||||||
|
} from "@/fetchers/custom/context/dash/user/fetch";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const FETCH_TIMEOUT = 5000; // 5 seconds timeout
|
||||||
|
|
||||||
|
// Default user state to use when API calls fail
|
||||||
|
const DEFAULT_USER_STATE: ClientUser = {
|
||||||
|
uuid: "default-user-id",
|
||||||
|
avatar: "",
|
||||||
|
email: "",
|
||||||
|
phone_number: "",
|
||||||
|
user_tag: "guest",
|
||||||
|
password_expiry_begins: new Date().toISOString(),
|
||||||
|
person: {
|
||||||
|
uuid: "default-person-id",
|
||||||
|
firstname: "Guest",
|
||||||
|
surname: "User",
|
||||||
|
middle_name: "",
|
||||||
|
sex_code: "",
|
||||||
|
person_tag: "guest",
|
||||||
|
country_code: "",
|
||||||
|
birth_date: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Client-side fetch function that uses the server-side implementation
|
||||||
|
async function checkContextDashUserInfo(): Promise<ClientUser> {
|
||||||
|
try {
|
||||||
|
console.log("Fetching user data using server-side function");
|
||||||
|
|
||||||
|
// First try to use the server-side implementation
|
||||||
|
try {
|
||||||
|
const serverData = await getUserFromServer();
|
||||||
|
console.log(
|
||||||
|
"User data from server:",
|
||||||
|
JSON.stringify(serverData, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we got valid data from the server, return it
|
||||||
|
if (serverData && serverData.uuid) {
|
||||||
|
// Check if we have a real user (not the default)
|
||||||
|
if (
|
||||||
|
serverData.uuid !== "default-user-id" ||
|
||||||
|
serverData.email ||
|
||||||
|
(serverData.person &&
|
||||||
|
(serverData.person.firstname !== "Guest" ||
|
||||||
|
serverData.person.surname !== "User"))
|
||||||
|
) {
|
||||||
|
console.log("Valid user data found from server");
|
||||||
|
return serverData;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"Default user data returned from server, falling back to client-side"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Invalid user data structure from server");
|
||||||
|
}
|
||||||
|
} catch (serverError) {
|
||||||
|
console.warn(
|
||||||
|
"Error using server-side user data fetch, falling back to client-side:",
|
||||||
|
serverError
|
||||||
|
);
|
||||||
|
// Continue to client-side implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to client-side implementation
|
||||||
|
console.log(
|
||||||
|
`Falling back to client-side fetch: ${API_BASE_URL}/context/dash/user`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an AbortController to handle timeouts
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
console.log("User data API response:", data);
|
||||||
|
|
||||||
|
// Handle different response formats
|
||||||
|
if (data.status === 200 && data.data) {
|
||||||
|
// Standard API response format
|
||||||
|
return data.data;
|
||||||
|
} else if (data.user) {
|
||||||
|
// Direct Redis object format
|
||||||
|
console.log("Found user data in Redis format");
|
||||||
|
return data.user;
|
||||||
|
} else if (data.uuid) {
|
||||||
|
// Direct user object format
|
||||||
|
console.log("Found direct user data format");
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
console.warn("Invalid response format from user API");
|
||||||
|
return DEFAULT_USER_STATE;
|
||||||
|
}
|
||||||
|
} catch (fetchError) {
|
||||||
|
// Clear the timeout if it hasn't fired yet
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Check if this is an abort error (timeout)
|
||||||
|
if (
|
||||||
|
fetchError instanceof DOMException &&
|
||||||
|
fetchError.name === "AbortError"
|
||||||
|
) {
|
||||||
|
console.warn("Request timed out or was aborted");
|
||||||
|
return DEFAULT_USER_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-throw other errors to be caught by the outer catch
|
||||||
|
throw fetchError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Handle all other errors
|
||||||
|
console.error(
|
||||||
|
"Error fetching user data:",
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
);
|
||||||
|
return DEFAULT_USER_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setContextDashUserInfo({
|
||||||
|
userSet,
|
||||||
|
}: {
|
||||||
|
userSet: ClientUser;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log("Setting user data using server-side function");
|
||||||
|
|
||||||
|
// First try to use the server-side implementation
|
||||||
|
try {
|
||||||
|
const success = await setUserFromServer(userSet);
|
||||||
|
if (success) {
|
||||||
|
console.log(
|
||||||
|
"Successfully updated user data using server-side function"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (serverError) {
|
||||||
|
console.warn(
|
||||||
|
"Error using server-side user data update, falling back to client-side:",
|
||||||
|
serverError
|
||||||
|
);
|
||||||
|
// Continue to client-side implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to client-side implementation
|
||||||
|
console.log(
|
||||||
|
`Falling back to client-side update: ${API_BASE_URL}/context/dash/user`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an AbortController to handle timeouts
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch(`${API_BASE_URL}/context/dash/user`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(userSet),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the timeout
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${result.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json();
|
||||||
|
console.log("Update user data API response:", data);
|
||||||
|
|
||||||
|
return data.status === 200;
|
||||||
|
} catch (fetchError) {
|
||||||
|
// Clear the timeout if it hasn't fired yet
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Check if this is an abort error (timeout)
|
||||||
|
if (
|
||||||
|
fetchError instanceof DOMException &&
|
||||||
|
fetchError.name === "AbortError"
|
||||||
|
) {
|
||||||
|
console.warn("Request timed out or was aborted");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-throw other errors to be caught by the outer catch
|
||||||
|
throw fetchError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error setting user data:",
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the user hook using the factory with custom fetch functions
|
||||||
|
const useContextUser = createContextHook<ClientUser>({
|
||||||
|
endpoint: "/context/dash/user",
|
||||||
|
contextName: "user",
|
||||||
|
enablePeriodicRefresh: false,
|
||||||
|
// Use our improved fetch functions
|
||||||
|
customFetch: checkContextDashUserInfo,
|
||||||
|
customUpdate: async (newData: ClientUser) => {
|
||||||
|
return await setContextDashUserInfo({ userSet: newData });
|
||||||
|
},
|
||||||
|
// Provide default value
|
||||||
|
defaultValue: DEFAULT_USER_STATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom hook for user data with the expected interface
|
||||||
|
interface UseUserResult {
|
||||||
|
userData: ClientUser | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
refreshUser: () => Promise<void>;
|
||||||
|
updateUser: (newUser: ClientUser) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper hook that adapts the generic hook to the expected interface
|
||||||
|
export function useUser(): UseUserResult {
|
||||||
|
const { data, isLoading, error, refresh, update } = useContextUser();
|
||||||
|
|
||||||
|
return {
|
||||||
|
userData: data,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refreshUser: refresh,
|
||||||
|
updateUser: update,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkContextDashUserInfo, setContextDashUserInfo };
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
'use client';
|
||||||
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent } from "@/components/mutual/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/mutual/ui/button";
|
||||||
|
import { languageSelectionTranslation } from "@/languages/mutual/languageSelection";
|
||||||
|
import { langGetKey, langGet } from "@/lib/langGet";
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
import LanguageSelectionItem from "./languageItem";
|
||||||
|
|
||||||
|
interface LanguageSelectionComponentProps {
|
||||||
|
activePage: string;
|
||||||
|
onlineData: any;
|
||||||
|
onlineLoading: boolean;
|
||||||
|
onlineError: any;
|
||||||
|
refreshOnline: () => Promise<void>;
|
||||||
|
updateOnline: (newOnline: any) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({ activePage, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||||
|
const lang = onlineData?.lang as LanguageTypes || 'en';
|
||||||
|
const translations = langGet(lang, languageSelectionTranslation);
|
||||||
|
const languageButtons = [
|
||||||
|
{ activeLang: lang, buttonsLang: "en", refUrl: "en", innerText: langGetKey(translations, "english") },
|
||||||
|
{ activeLang: lang, buttonsLang: "tr", refUrl: "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>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{languageButtons.map((props, index) => (
|
||||||
|
<LanguageSelectionItem key={props.buttonsLang} {...props}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSelectionComponent;
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use client';
|
||||||
|
import { FC } from "react";
|
||||||
|
import { DropdownMenuItem } from "@/components/mutual/ui/dropdown-menu";
|
||||||
|
|
||||||
|
interface LanguageSelectionItemProps {
|
||||||
|
activeLang: string,
|
||||||
|
buttonsLang: string,
|
||||||
|
refUrl: string,
|
||||||
|
innerText: string,
|
||||||
|
onlineData: any,
|
||||||
|
onlineLoading: boolean,
|
||||||
|
onlineError: any,
|
||||||
|
refreshOnline: () => Promise<void>,
|
||||||
|
updateOnline: (newOnline: any) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderButtonComponent: FC<LanguageSelectionItemProps> = (
|
||||||
|
{ activeLang, buttonsLang, refUrl, innerText, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||||
|
const setOnlineObject = async () => {
|
||||||
|
if (!onlineData || onlineLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Updating language to:", buttonsLang);
|
||||||
|
const success = await updateOnline({
|
||||||
|
...onlineData,
|
||||||
|
lang: buttonsLang,
|
||||||
|
lastAction: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log("Language updated successfully");
|
||||||
|
await refreshOnline();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to update language");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating language:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={setOnlineObject}
|
||||||
|
className="flex w-full h-12 items-center justify-center text-center text-md cursor-pointer">
|
||||||
|
{innerText}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageSelectionItem: React.FC<LanguageSelectionItemProps> = ({ activeLang, buttonsLang, refUrl, innerText, onlineData, onlineLoading, onlineError, refreshOnline, updateOnline }) => {
|
||||||
|
|
||||||
|
const currentLang = onlineData?.lang || activeLang;
|
||||||
|
const isActive = buttonsLang !== currentLang;
|
||||||
|
const RenderButtonProp = {
|
||||||
|
refUrl,
|
||||||
|
innerText,
|
||||||
|
activeLang,
|
||||||
|
buttonsLang,
|
||||||
|
onlineData,
|
||||||
|
onlineLoading,
|
||||||
|
onlineError,
|
||||||
|
refreshOnline,
|
||||||
|
updateOnline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only render the button if it's not the current language
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isActive ? (
|
||||||
|
<div><RenderButtonComponent {...RenderButtonProp} /></div>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
disabled={!isActive}
|
||||||
|
className="flex w-full h-12 items-center justify-center text-center text-md opacity-50 cursor-not-allowed">
|
||||||
|
{innerText}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,22 @@
|
||||||
|
'use client';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { OnlineProvider } from '@/components/mutual/context/online/provider';
|
||||||
|
|
||||||
|
interface ClientProvidersProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientProviders({ children }: ClientProvidersProps) {
|
||||||
|
// Log provider initialization for debugging
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('ClientProviders initialized');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnlineProvider>
|
||||||
|
{children}
|
||||||
|
</OnlineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClientProviders;
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { ClientRedisOnline } from "@/fetchers/types/context/online/validations";
|
|
||||||
import { ClientPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
import { ClientPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
||||||
import { ClientMenu } from "@/fetchers/types/context/menu/validations";
|
import { ClientMenu } from "@/fetchers/types/context/menu/validations";
|
||||||
import { ClientHeader } from "@/fetchers/types/context/header/validations";
|
import { ClientHeader } from "@/fetchers/types/context/header/validations";
|
||||||
import { ClientSelection } from "@/fetchers/types/context/selection/validations";
|
import { ClientSelection } from "@/fetchers/types/context/selection/validations";
|
||||||
import { ClientUser } from "@/fetchers/types/context/user/validations";
|
import { ClientUser } from "@/fetchers/types/context/user/validations";
|
||||||
import { ClientSettings } from "@/fetchers/types/context/settings/validations";
|
import { ClientSettings } from "@/fetchers/types/context/settings/validations";
|
||||||
|
import { ClientOnline } from "@/fetchers/types/context/online/validations";
|
||||||
|
|
||||||
import { defaultValuesOnline } from "@/fetchers/types/context/online/validations";
|
import { defaultValuesOnline } from "@/fetchers/types/context/online/validations";
|
||||||
import { defaultValuesPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
import { defaultValuesPageConfig } from "@/fetchers/types/context/pageConfig/validations";
|
||||||
import { defaultValuesMenu } from "@/fetchers/types/context/menu/validations";
|
import { defaultValuesMenu } from "@/fetchers/types/context/menu/validations";
|
||||||
|
|
@ -14,7 +15,7 @@ import { defaultValuesUser } from "@/fetchers/types/context/user/validations";
|
||||||
import { defaultValuesSettings } from "@/fetchers/types/context/settings/validations";
|
import { defaultValuesSettings } from "@/fetchers/types/context/settings/validations";
|
||||||
|
|
||||||
interface ClientRedisToken {
|
interface ClientRedisToken {
|
||||||
online: ClientRedisOnline;
|
online: ClientOnline;
|
||||||
pageConfig: ClientPageConfig;
|
pageConfig: ClientPageConfig;
|
||||||
menu: ClientMenu;
|
menu: ClientMenu;
|
||||||
header: ClientHeader;
|
header: ClientHeader;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
export async function retrieveAvailableApplications(data: any): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://localhost:3000/api/menu", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "no-cache": "true" },
|
||||||
|
body: JSON.stringify({ ...data }),
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Failed to retrieve available applications");
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data
|
||||||
|
} catch (error) { throw error }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrievePageToRender(data: any): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://localhost:3000/api/pages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "no-cache": "true" },
|
||||||
|
body: JSON.stringify({ ...data }),
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Failed to retrieve page to render");
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data
|
||||||
|
} catch (error) { throw error }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
const buildingEn = {
|
||||||
|
building: "Building First Layer Label",
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildingPartsEn = {
|
||||||
|
...buildingEn,
|
||||||
|
parts: "Parts Second Layer Label",
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildingPartsFieldsEn = {
|
||||||
|
"Users.uuid": "UUID",
|
||||||
|
"Users.firstName": "First Name",
|
||||||
|
"Users.lastName": "Last Name",
|
||||||
|
"Users.email": "Email",
|
||||||
|
"Users.phoneNumber": "Phone Number",
|
||||||
|
"Users.country": "Country",
|
||||||
|
"Users.description": "Description",
|
||||||
|
"Users.isDeleted": "Is Deleted",
|
||||||
|
"Users.isConfirmed": "Is Confirmed",
|
||||||
|
"Users.createdAt": "Created At",
|
||||||
|
"Users.updatedAt": "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { buildingEn, buildingPartsEn, buildingPartsFieldsEn };
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
const buildingTr = {
|
||||||
|
building: "Bina Birinci Seviye",
|
||||||
|
};
|
||||||
|
const buildingPartsTr = {
|
||||||
|
...buildingTr,
|
||||||
|
parts: "Parçalar İkinci Seviye",
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildingPartsFieldsTr = {
|
||||||
|
"Users.uuid": "UUID",
|
||||||
|
"Users.firstName": "Ad",
|
||||||
|
"Users.lastName": "Soyad",
|
||||||
|
"Users.email": "Email",
|
||||||
|
"Users.phoneNumber": "Telefon Numarası",
|
||||||
|
"Users.country": "Ülke",
|
||||||
|
"Users.description": "Açıklama",
|
||||||
|
"Users.isDeleted": "Silindi",
|
||||||
|
"Users.isConfirmed": "Onaylandı",
|
||||||
|
"Users.createdAt": "Oluşturulma Tarihi",
|
||||||
|
"Users.updatedAt": "Güncellenme Tarihi",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { buildingTr, buildingPartsTr, buildingPartsFieldsTr };
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { LanguageTypes } from "@/validations/mutual/language/validations";
|
||||||
|
import { DynamicPage } from "@/validations/mutual/menu/menu";
|
||||||
|
import { managementAccountTenantMain } from "./management/account/tenantSomething/index";
|
||||||
|
// import { managementAccountTenantMainSecond } from "./management/account/tenantSomethingSecond/index";
|
||||||
|
// import { buildingPartsTenantSomething } from "./building/parts/tenantSomething/index";
|
||||||
|
|
||||||
|
const dynamicPagesIndex: Record<string, Record<LanguageTypes, DynamicPage>> = {
|
||||||
|
"/main/pages/user/dashboard": managementAccountTenantMain,
|
||||||
|
"/definitions/identifications/people": managementAccountTenantMain,
|
||||||
|
"/definitions/identifications/users": managementAccountTenantMain,
|
||||||
|
"/definitions/building/parts": managementAccountTenantMain,
|
||||||
|
"/definitions/building/areas": managementAccountTenantMain,
|
||||||
|
"/building/accounts/managment/accounts": managementAccountTenantMain,
|
||||||
|
"/building/accounts/managment/budgets": managementAccountTenantMain,
|
||||||
|
"/building/accounts/parts/accounts": managementAccountTenantMain,
|
||||||
|
"/building/accounts/parts/budgets": managementAccountTenantMain,
|
||||||
|
"/building/meetings/regular/actions": managementAccountTenantMain,
|
||||||
|
"/building/meetings/regular/accounts": managementAccountTenantMain,
|
||||||
|
"/building/meetings/ergunt/actions": managementAccountTenantMain,
|
||||||
|
"/building/meetings/ergunt/accounts": managementAccountTenantMain,
|
||||||
|
"/building/meetings/invited/attendance": managementAccountTenantMain,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dynamicPagesIndex };
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { footerDefaultEn } from "@/languages/mutual/footer/english";
|
||||||
|
import { headerDefaultEn } from "@/languages/mutual/header/english";
|
||||||
|
import { managementAccountEn, managementAccountFieldsEn } from "../../english";
|
||||||
|
import { contentDefaultEn } from "@/languages/mutual/content/english";
|
||||||
|
|
||||||
|
const contentManagementAccountTenantSomethingEn = {
|
||||||
|
...managementAccountFieldsEn,
|
||||||
|
title: "Management Account Tenant Something",
|
||||||
|
content: "Management Account Tenant Something Content",
|
||||||
|
button: "Management Account Tenant Something Button",
|
||||||
|
};
|
||||||
|
const footerManagementAccountTenantSomethingEn = {
|
||||||
|
...footerDefaultEn,
|
||||||
|
page: "Management Account Tenant Something Footer",
|
||||||
|
};
|
||||||
|
const headerManagementAccountTenantSomethingEn = {
|
||||||
|
...headerDefaultEn,
|
||||||
|
page: "Management Account Tenant Something Header",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuManagementAccountTenantSomethingEn = {
|
||||||
|
...managementAccountEn,
|
||||||
|
"tenant/something": "Tenant Info",
|
||||||
|
};
|
||||||
|
|
||||||
|
const managementAccountTenantMainEn = {
|
||||||
|
header: headerManagementAccountTenantSomethingEn,
|
||||||
|
menu: menuManagementAccountTenantSomethingEn,
|
||||||
|
content: contentManagementAccountTenantSomethingEn,
|
||||||
|
footer: footerManagementAccountTenantSomethingEn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
contentManagementAccountTenantSomethingEn,
|
||||||
|
footerManagementAccountTenantSomethingEn,
|
||||||
|
headerManagementAccountTenantSomethingEn,
|
||||||
|
menuManagementAccountTenantSomethingEn,
|
||||||
|
managementAccountTenantMainEn,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { managementAccountTenantMainTr } from "./turkish";
|
||||||
|
import { managementAccountTenantMainEn } from "./english";
|
||||||
|
|
||||||
|
const managementAccountTenantMain = {
|
||||||
|
tr: managementAccountTenantMainTr,
|
||||||
|
en: managementAccountTenantMainEn,
|
||||||
|
}
|
||||||
|
|
||||||
|
export { managementAccountTenantMain }
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { footerDefaultTr } from "@/languages/mutual/footer/turkish";
|
||||||
|
import { headerDefaultTr } from "@/languages/mutual/header/turkish";
|
||||||
|
import { managementAccountTr } from "../../turkish";
|
||||||
|
|
||||||
|
const contentManagementAccountTenantSomethingTr = {
|
||||||
|
title: "Yönetim Hesap Kiracı Bilgileri",
|
||||||
|
description: "Yönetim Hesap Kiracı Bilgileri",
|
||||||
|
button: "Yönetim Hesap Kiracı Bilgileri Buton",
|
||||||
|
};
|
||||||
|
const footerManagementAccountTenantSomethingTr = {
|
||||||
|
...footerDefaultTr,
|
||||||
|
page: "Yönetim Hesap Kiracı Bilgileri Footer",
|
||||||
|
};
|
||||||
|
const headerManagementAccountTenantSomethingTr = {
|
||||||
|
...headerDefaultTr,
|
||||||
|
page: "Yönetim Hesap Kiracı Bilgileri Header",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuManagementAccountTenantSomethingTr = {
|
||||||
|
...managementAccountTr,
|
||||||
|
"tenant/something": "Kiracı Bilgileri",
|
||||||
|
};
|
||||||
|
const managementAccountTenantMainTr = {
|
||||||
|
header: headerManagementAccountTenantSomethingTr,
|
||||||
|
menu: menuManagementAccountTenantSomethingTr,
|
||||||
|
content: contentManagementAccountTenantSomethingTr,
|
||||||
|
footer: footerManagementAccountTenantSomethingTr,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
contentManagementAccountTenantSomethingTr,
|
||||||
|
footerManagementAccountTenantSomethingTr,
|
||||||
|
headerManagementAccountTenantSomethingTr,
|
||||||
|
menuManagementAccountTenantSomethingTr,
|
||||||
|
managementAccountTenantMainTr,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { footerDefaultEn } from "@/languages/mutual/footer/english";
|
||||||
|
import { headerDefaultEn } from "@/languages/mutual/header/english";
|
||||||
|
import { contentDefaultEn } from "@/languages/mutual/content/english";
|
||||||
|
import { managementAccountEn, managementAccountFieldsEn } from "../../english";
|
||||||
|
|
||||||
|
const contentManagementAccountTenantSomethingSecondEn = {
|
||||||
|
...contentDefaultEn,
|
||||||
|
...managementAccountFieldsEn,
|
||||||
|
title: "Management Account Tenant Something",
|
||||||
|
content: "Management Account Tenant Something Content",
|
||||||
|
button: "Management Account Tenant Something Button",
|
||||||
|
};
|
||||||
|
const footerManagementAccountTenantSomethingSecondEn = {
|
||||||
|
...footerDefaultEn,
|
||||||
|
page: "Management Account Tenant Something Second Footer",
|
||||||
|
};
|
||||||
|
const headerManagementAccountTenantSomethingSecondEn = {
|
||||||
|
...headerDefaultEn,
|
||||||
|
page: "Management Account Tenant Something Second Header",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuManagementAccountTenantSomethingSecondEn = {
|
||||||
|
...managementAccountEn,
|
||||||
|
"tenant/somethingSecond": "Tenant Info Second",
|
||||||
|
};
|
||||||
|
|
||||||
|
const managementAccountTenantMainSecondEn = {
|
||||||
|
header: headerManagementAccountTenantSomethingSecondEn,
|
||||||
|
menu: menuManagementAccountTenantSomethingSecondEn,
|
||||||
|
content: contentManagementAccountTenantSomethingSecondEn,
|
||||||
|
footer: footerManagementAccountTenantSomethingSecondEn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
contentManagementAccountTenantSomethingSecondEn,
|
||||||
|
footerManagementAccountTenantSomethingSecondEn,
|
||||||
|
headerManagementAccountTenantSomethingSecondEn,
|
||||||
|
menuManagementAccountTenantSomethingSecondEn,
|
||||||
|
managementAccountTenantMainSecondEn,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { managementAccountTenantMainSecondTr } from "./turkish";
|
||||||
|
import { managementAccountTenantMainSecondEn } from "./english";
|
||||||
|
|
||||||
|
const managementAccountTenantMainSecond = {
|
||||||
|
tr: managementAccountTenantMainSecondTr,
|
||||||
|
en: managementAccountTenantMainSecondEn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { managementAccountTenantMainSecond };
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { footerDefaultTr } from "@/languages/mutual/footer/turkish";
|
||||||
|
import { headerDefaultTr } from "@/languages/mutual/header/turkish";
|
||||||
|
import { managementAccountTr, managementAccountFieldsTr } from "../../turkish";
|
||||||
|
|
||||||
|
const contentManagementAccountTenantSomethingSecondTr = {
|
||||||
|
...managementAccountFieldsTr,
|
||||||
|
title: "Yönetim Hesap Kiracı Bilgileri",
|
||||||
|
description: "Yönetim Hesap Kiracı Bilgileri",
|
||||||
|
button: "Yönetim Hesap Kiracı Bilgileri Buton",
|
||||||
|
};
|
||||||
|
const footerManagementAccountTenantSomethingSecondTr = {
|
||||||
|
...footerDefaultTr,
|
||||||
|
page: "Yönetim Hesap Kiracı Bilgileri Footer",
|
||||||
|
};
|
||||||
|
const headerManagementAccountTenantSomethingSecondTr = {
|
||||||
|
...headerDefaultTr,
|
||||||
|
page: "Yönetim Hesap Kiracı Bilgileri Header",
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuManagementAccountTenantSomethingSecondTr = {
|
||||||
|
...managementAccountTr,
|
||||||
|
"tenant/somethingSecond": "İkinci Kiracı Bilgileri",
|
||||||
|
};
|
||||||
|
const managementAccountTenantMainSecondTr = {
|
||||||
|
header: headerManagementAccountTenantSomethingSecondTr,
|
||||||
|
menu: menuManagementAccountTenantSomethingSecondTr,
|
||||||
|
content: contentManagementAccountTenantSomethingSecondTr,
|
||||||
|
footer: footerManagementAccountTenantSomethingSecondTr,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
contentManagementAccountTenantSomethingSecondTr,
|
||||||
|
footerManagementAccountTenantSomethingSecondTr,
|
||||||
|
headerManagementAccountTenantSomethingSecondTr,
|
||||||
|
menuManagementAccountTenantSomethingSecondTr,
|
||||||
|
managementAccountTenantMainSecondTr,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
const managementEn = {
|
||||||
|
management: "Management First Layer Label",
|
||||||
|
};
|
||||||
|
|
||||||
|
const managementAccountEn = {
|
||||||
|
...managementEn,
|
||||||
|
account: "Account Second Layer Label",
|
||||||
|
};
|
||||||
|
|
||||||
|
const managementAccountFieldsEn = {
|
||||||
|
"User.firstName": "First Name",
|
||||||
|
"User.lastName": "Last Name",
|
||||||
|
"User.email": "Email",
|
||||||
|
"User.phoneNumber": "Phone Number",
|
||||||
|
"User.country": "Country",
|
||||||
|
"User.description": "Description",
|
||||||
|
"User.isDeleted": "Is Deleted",
|
||||||
|
"User.isConfirmed": "Is Confirmed",
|
||||||
|
"User.createdAt": "Created At",
|
||||||
|
"User.updatedAt": "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { managementEn, managementAccountEn, managementAccountFieldsEn };
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
const managementTr = {
|
||||||
|
management: "Management Birinci Seviye",
|
||||||
|
};
|
||||||
|
const managementAccountTr = {
|
||||||
|
...managementTr,
|
||||||
|
account: "Account İkinci Seviye",
|
||||||
|
};
|
||||||
|
|
||||||
|
const managementAccountFieldsTr = {
|
||||||
|
"User.firstName": "Ad",
|
||||||
|
"User.lastName": "Soyad",
|
||||||
|
"User.email": "Email",
|
||||||
|
"User.phoneNumber": "Telefon Numarası",
|
||||||
|
"User.country": "Ülke",
|
||||||
|
"User.description": "Açıklama",
|
||||||
|
"User.isDeleted": "Silindi",
|
||||||
|
"User.isConfirmed": "Onaylandı",
|
||||||
|
"User.createdAt": "Oluşturulma Tarihi",
|
||||||
|
"User.updatedAt": "Güncellenme Tarihi",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { managementTr, managementAccountTr, managementAccountFieldsTr };
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
const contentDefaultEn = {
|
||||||
|
title: "Content Default",
|
||||||
|
content: "Content Default",
|
||||||
|
button: "Content Default",
|
||||||
|
rows: "Rows",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { contentDefaultEn };
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { contentDefaultTr } from "./turkish";
|
||||||
|
import { contentDefaultEn } from "./english";
|
||||||
|
|
||||||
|
const contentDefault = {
|
||||||
|
tr: contentDefaultTr,
|
||||||
|
en: contentDefaultEn,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { contentDefault };
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
const contentDefaultTr = {
|
||||||
|
title: "İçerik Varsayılan",
|
||||||
|
content: "İçerik Varsayılan",
|
||||||
|
button: "İçerik Varsayılan",
|
||||||
|
rows: "Satır",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { contentDefaultTr };
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const dashboardTranslationEn = {
|
||||||
|
title: "Dashboard Panel",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dashboardTranslationEn };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { dashboardTranslationEn } from "./english";
|
||||||
|
import { dashboardTranslationTr } from "./turkish";
|
||||||
|
|
||||||
|
export const dashboardTranslation = {
|
||||||
|
en: dashboardTranslationEn,
|
||||||
|
tr: dashboardTranslationTr,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const dashboardTranslationTr = {
|
||||||
|
title: "Yönetim Panosu",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dashboardTranslationTr };
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
const footerDefaultEn = {
|
||||||
|
description: "Footer Default",
|
||||||
|
footer: "Footer Info",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { footerDefaultEn };
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
const footerDefaultTr = {
|
||||||
|
description: "Footer Bilgi",
|
||||||
|
footer: "Alt Bilgi",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { footerDefaultTr };
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const headerDefaultEn = {
|
||||||
|
selectedPage: "Selected Page",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { headerDefaultEn };
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const headerDefaultTr = {
|
||||||
|
selectedPage: "Seçili Sayfa",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { headerDefaultTr };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const languageSelectionTranslationEn = {
|
||||||
|
title: "Language Selection",
|
||||||
|
english: "English",
|
||||||
|
turkish: "Turkish",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { languageSelectionTranslationEn };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { languageSelectionTranslationEn } from "./english";
|
||||||
|
import { languageSelectionTranslationTr } from "./turkish";
|
||||||
|
|
||||||
|
export const languageSelectionTranslation = {
|
||||||
|
en: languageSelectionTranslationEn,
|
||||||
|
tr: languageSelectionTranslationTr
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
const languageSelectionTranslationTr = {
|
||||||
|
title: "Dil Seçimi",
|
||||||
|
english: "İngilizce",
|
||||||
|
turkish: "Türkçe",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { languageSelectionTranslationTr };
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
// const menuTranslationEn = {
|
||||||
|
// "/definitions/identifications/people": "People",
|
||||||
|
// "/definitions/identifications/users": "Users",
|
||||||
|
|
||||||
|
// "/definitions/building/parts": "Build Parts",
|
||||||
|
// "/definitions/building/areas": "Building Areas",
|
||||||
|
|
||||||
|
// "/building/accounts/managment/accounts": "Management Accounts",
|
||||||
|
// "/building/accounts/managment/budgets": "Management Budgets",
|
||||||
|
// "/building/accounts/parts/accounts": "Parts Accounts",
|
||||||
|
// "/building/accounts/parts/budgets": "Parts Budgets",
|
||||||
|
|
||||||
|
// "/building/meetings/regular/actions": "Regular Meeting Actions",
|
||||||
|
// "/building/meetings/regular/accounts": "Regular Meeting Accounts",
|
||||||
|
// "/building/meetings/ergunt/actions": "Ergunt Meeting Actions",
|
||||||
|
// "/building/meetings/ergunt/accounts": "Ergunt Meeting Accounts",
|
||||||
|
// "/building/meetings/invited/attendance": "Meeting Invited Attendance",
|
||||||
|
// };
|
||||||
|
|
||||||
|
const menuTranslationEn = {
|
||||||
|
// New menu
|
||||||
|
"/dashboard": [
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
],
|
||||||
|
"/individual": [
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
],
|
||||||
|
"/user": [
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
],
|
||||||
|
"/build": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
],
|
||||||
|
"/build/parts": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Parts", key: "parts" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
],
|
||||||
|
"/management/budget/actions": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Actions", key: "actions" },
|
||||||
|
],
|
||||||
|
"/management/budget": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
],
|
||||||
|
"/annual/meeting/close": [
|
||||||
|
{ value: "Annual", key: "annual" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Close", key: "close" },
|
||||||
|
],
|
||||||
|
"/emergency/meeting": [
|
||||||
|
{ value: "Emergency", key: "emergency" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
],
|
||||||
|
"/emergency/meeting/close": [
|
||||||
|
{ value: "Emergency", key: "emergency" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Close", key: "close" },
|
||||||
|
],
|
||||||
|
"/tenant/accounting": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
],
|
||||||
|
"/meeting/participation": [
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Participation", key: "participation" },
|
||||||
|
{ value: "Participation", key: "participation" },
|
||||||
|
],
|
||||||
|
"/tenant/messageToBM": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Message To BM", key: "messageToBM" },
|
||||||
|
{ value: "Message To BM", key: "messageToBM" },
|
||||||
|
],
|
||||||
|
"/tenant/messageToOwner": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Message To Owner", key: "messageToOwner" },
|
||||||
|
{ value: "Message To Owner", key: "messageToOwner" },
|
||||||
|
],
|
||||||
|
"/management/accounting": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
],
|
||||||
|
"/build/area": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Area", key: "area" },
|
||||||
|
{ value: "Area", key: "area" },
|
||||||
|
],
|
||||||
|
"/management/budget/status": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Status", key: "status" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Early menu
|
||||||
|
"/definitions/identifications/people": [
|
||||||
|
{ value: "Definitions", key: "definitions" },
|
||||||
|
{ value: "Identifications", key: "identifications" },
|
||||||
|
{ value: "People", key: "people" },
|
||||||
|
],
|
||||||
|
"/definitions/identifications/users": [
|
||||||
|
{ value: "Definitions", key: "definitions" },
|
||||||
|
{ value: "Identifications", key: "identifications" },
|
||||||
|
{ value: "Users", key: "users" },
|
||||||
|
],
|
||||||
|
"/definitions/building/parts": [
|
||||||
|
{ value: "Definitions", key: "definitions" },
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Parts", key: "parts" },
|
||||||
|
],
|
||||||
|
"/definitions/building/areas": [
|
||||||
|
{ value: "Definitions", key: "definitions" },
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Areas", key: "areas" },
|
||||||
|
],
|
||||||
|
"/building/accounts/managment/accounts": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Accounts", key: "accounts" },
|
||||||
|
{ value: "Managment", key: "managment" },
|
||||||
|
],
|
||||||
|
"/building/accounts/managment/budgets": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Accounts", key: "accounts" },
|
||||||
|
{ value: "Managment", key: "managment" },
|
||||||
|
],
|
||||||
|
"/building/accounts/parts/accounts": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Accounts", key: "accounts" },
|
||||||
|
{ value: "Parts", key: "parts" },
|
||||||
|
],
|
||||||
|
"/building/accounts/parts/budgets": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Accounts", key: "accounts" },
|
||||||
|
{ value: "Parts", key: "parts" },
|
||||||
|
],
|
||||||
|
"/building/meetings/regular/actions": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Meetings", key: "meetings" },
|
||||||
|
{ value: "Regular", key: "regular" },
|
||||||
|
],
|
||||||
|
"/building/meetings/regular/accounts": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Meetings", key: "meetings" },
|
||||||
|
{ value: "Regular", key: "regular" },
|
||||||
|
],
|
||||||
|
"/building/meetings/ergunt/actions": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Meetings", key: "meetings" },
|
||||||
|
{ value: "Ergunt", key: "ergunt" },
|
||||||
|
],
|
||||||
|
"/building/meetings/ergunt/accounts": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Meetings", key: "meetings" },
|
||||||
|
{ value: "Ergunt", key: "ergunt" },
|
||||||
|
],
|
||||||
|
"/building/meetings/invited/attendance": [
|
||||||
|
{ value: "Building", key: "building" },
|
||||||
|
{ value: "Meetings", key: "meetings" },
|
||||||
|
{ value: "Invited", key: "invited" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export { menuTranslationEn };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { menuTranslationEn } from "./english";
|
||||||
|
import { menuTranslationTr } from "./turkish";
|
||||||
|
|
||||||
|
export const menuTranslation = {
|
||||||
|
en: menuTranslationEn,
|
||||||
|
tr: menuTranslationTr,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
// const menuTranslationTr = {
|
||||||
|
// "/definitions/identifications/people": "Kişiler",
|
||||||
|
// "/definitions/identifications/users": "Kullanıcılar",
|
||||||
|
|
||||||
|
// "/definitions/building/parts": "Daireler",
|
||||||
|
// "/definitions/building/areas": "Bina Alanları",
|
||||||
|
|
||||||
|
// "/building/accounts/managment/accounts": "Bina Hesapları",
|
||||||
|
// "/building/accounts/managment/budgets": "Bina Bütçesi",
|
||||||
|
// "/building/accounts/parts/accounts": "Daire Hesapları",
|
||||||
|
// "/building/accounts/parts/budgets": "Daire Bütçesi",
|
||||||
|
|
||||||
|
// "/building/meetings/regular/actions": "Düzenli Toplantı Eylemleri",
|
||||||
|
// "/building/meetings/regular/accounts": "Düzenli Toplantı Accounts",
|
||||||
|
// "/building/meetings/ergunt/actions": "Ergunt Toplantı Eylemleri",
|
||||||
|
// "/building/meetings/ergunt/accounts": "Ergunt Toplantı Accounts",
|
||||||
|
// "/building/meetings/invited/attendance": "Toplantı Davetli Katılımlar",
|
||||||
|
// };
|
||||||
|
|
||||||
|
const menuTranslationTr = {
|
||||||
|
// New menu
|
||||||
|
"/dashboard": [
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
{ value: "Dashboard", key: "dashboard" },
|
||||||
|
],
|
||||||
|
"/individual": [
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
{ value: "Individual", key: "individual" },
|
||||||
|
],
|
||||||
|
"/user": [
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
{ value: "User", key: "user" },
|
||||||
|
],
|
||||||
|
"/build": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
],
|
||||||
|
"/build/parts": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Parts", key: "parts" },
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
],
|
||||||
|
"/management/budget/actions": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Actions", key: "actions" },
|
||||||
|
],
|
||||||
|
"/management/budget": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
],
|
||||||
|
"/annual/meeting/close": [
|
||||||
|
{ value: "Annual", key: "annual" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Close", key: "close" },
|
||||||
|
],
|
||||||
|
"/emergency/meeting": [
|
||||||
|
{ value: "Emergency", key: "emergency" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
],
|
||||||
|
"/emergency/meeting/close": [
|
||||||
|
{ value: "Emergency", key: "emergency" },
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Close", key: "close" },
|
||||||
|
],
|
||||||
|
"/tenant/accounting": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
],
|
||||||
|
"/meeting/participation": [
|
||||||
|
{ value: "Meeting", key: "meeting" },
|
||||||
|
{ value: "Participation", key: "participation" },
|
||||||
|
{ value: "Participation", key: "participation" },
|
||||||
|
],
|
||||||
|
"/tenant/messageToBM": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Message To BM", key: "messageToBM" },
|
||||||
|
{ value: "Message To BM", key: "messageToBM" },
|
||||||
|
],
|
||||||
|
"/tenant/messageToOwner": [
|
||||||
|
{ value: "Tenant", key: "tenant" },
|
||||||
|
{ value: "Message To Owner", key: "messageToOwner" },
|
||||||
|
{ value: "Message To Owner", key: "messageToOwner" },
|
||||||
|
],
|
||||||
|
"/management/accounting": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
{ value: "Accounting", key: "accounting" },
|
||||||
|
],
|
||||||
|
"/build/area": [
|
||||||
|
{ value: "Build", key: "build" },
|
||||||
|
{ value: "Area", key: "area" },
|
||||||
|
{ value: "Area", key: "area" },
|
||||||
|
],
|
||||||
|
"/management/budget/status": [
|
||||||
|
{ value: "Management", key: "management" },
|
||||||
|
{ value: "Budget", key: "budget" },
|
||||||
|
{ value: "Status", key: "status" },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Early menu
|
||||||
|
"/definitions/identifications/people": [
|
||||||
|
{ value: "Tanımlamalar", key: "definitions" },
|
||||||
|
{ value: "Tanımlamalar", key: "identifications" },
|
||||||
|
{ value: "Kişiler", key: "people" },
|
||||||
|
],
|
||||||
|
"/definitions/identifications/users": [
|
||||||
|
{ value: "Tanımlamalar", key: "definitions" },
|
||||||
|
{ value: "Tanımlamalar", key: "identifications" },
|
||||||
|
{ value: "Kullanıcılar", key: "users" },
|
||||||
|
],
|
||||||
|
"/definitions/building/parts": [
|
||||||
|
{ value: "Tanımlamalar", key: "definitions" },
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Daireler", key: "parts" },
|
||||||
|
],
|
||||||
|
"/definitions/building/areas": [
|
||||||
|
{ value: "Tanımlamalar", key: "definitions" },
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Bina Alanları", key: "areas" },
|
||||||
|
],
|
||||||
|
"/building/accounts/managment/accounts": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||||
|
{ value: "Yönetim", key: "managment" },
|
||||||
|
],
|
||||||
|
"/building/accounts/managment/budgets": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||||
|
{ value: "Yönetim", key: "managment" },
|
||||||
|
],
|
||||||
|
"/building/accounts/parts/accounts": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||||
|
{ value: "Daireler", key: "parts" },
|
||||||
|
],
|
||||||
|
"/building/accounts/parts/budgets": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Hesap Eylemleri", key: "accounts" },
|
||||||
|
{ value: "Daireler", key: "parts" },
|
||||||
|
],
|
||||||
|
"/building/meetings/regular/actions": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Toplantılar", key: "meetings" },
|
||||||
|
{ value: "Düzenli", key: "regular" },
|
||||||
|
],
|
||||||
|
"/building/meetings/regular/accounts": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Toplantılar", key: "meetings" },
|
||||||
|
{ value: "Düzenli", key: "regular" },
|
||||||
|
],
|
||||||
|
"/building/meetings/ergunt/actions": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Toplantılar", key: "meetings" },
|
||||||
|
{ value: "Acil", key: "ergunt" },
|
||||||
|
],
|
||||||
|
"/building/meetings/ergunt/accounts": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Toplantılar", key: "meetings" },
|
||||||
|
{ value: "Acil", key: "ergunt" },
|
||||||
|
],
|
||||||
|
"/building/meetings/invited/attendance": [
|
||||||
|
{ value: "Bina", key: "building" },
|
||||||
|
{ value: "Toplantılar", key: "meetings" },
|
||||||
|
{ value: "Davetli", key: "invited" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export { menuTranslationTr };
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const dashboardTranslationEn = {
|
||||||
|
title: "Dashboard Panel",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dashboardTranslationEn };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { dashboardTranslationEn } from "./english";
|
||||||
|
import { dashboardTranslationTr } from "./turkish";
|
||||||
|
|
||||||
|
export const dashboardTranslation = {
|
||||||
|
en: dashboardTranslationEn,
|
||||||
|
tr: dashboardTranslationTr,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const dashboardTranslationTr = {
|
||||||
|
title: "Yönetim Panosu",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dashboardTranslationTr };
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use server';
|
||||||
|
import { FC, Suspense } from "react";
|
||||||
|
import { AuthLayoutProps } from "@/validations/mutual/auth/props";
|
||||||
|
|
||||||
|
const AuthLayout: FC<AuthLayoutProps> = async ({ lang, page, activePageUrl }) => {
|
||||||
|
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>}>{page}</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthLayout };
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use client';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { ClientProviders } from "@/components/mutual/providers/client-providers";
|
||||||
|
import { useMenu } from "@/components/mutual/context/menu/context";
|
||||||
|
import { useOnline } from "@/components/mutual/context/online/context";
|
||||||
|
import { useSelection } from "@/components/mutual/context/selection/context";
|
||||||
|
import { useUser } from "@/components/mutual/context/user/context";
|
||||||
|
import { useConfig } from "@/components/mutual/context/config/context";
|
||||||
|
import { ModeTypes } from "@/validations/mutual/dashboard/props";
|
||||||
|
|
||||||
|
import HeaderComponent from "@/components/custom/header/component";
|
||||||
|
import MenuComponent from "@/components/custom/menu/component";
|
||||||
|
import ContentComponent from "@/components/custom/content/component";
|
||||||
|
import FooterComponent from "@/components/custom/footer/component";
|
||||||
|
|
||||||
|
interface ClientLayoutProps { activePageUrl: string, searchParams: Record<string, any> }
|
||||||
|
|
||||||
|
const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) => {
|
||||||
|
|
||||||
|
const { onlineData, isLoading: onlineLoading, error: onlineError, refreshOnline, updateOnline } = useOnline();
|
||||||
|
const { userData, isLoading: userLoading, error: userError, refreshUser, updateUser } = useUser();
|
||||||
|
const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu();
|
||||||
|
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
|
||||||
|
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
|
||||||
|
const prefix = "/panel"
|
||||||
|
const mode = (searchParams?.mode as ModeTypes) || 'shortList';
|
||||||
|
console.log("onlineData", onlineData)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientProviders>
|
||||||
|
<div className="flex flex-col min-w-screen">
|
||||||
|
<HeaderComponent activePageUrl={activePageUrl} searchParams={searchParams}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||||
|
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser} />
|
||||||
|
<MenuComponent availableApplications={availableApplications} activePageUrl={activePageUrl} prefix={prefix} searchParams={searchParams}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
|
||||||
|
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||||
|
selectionData={selectionData} selectionLoading={selectionLoading} selectionError={selectionError} refreshSelection={refreshSelection} updateSelection={updateSelection}
|
||||||
|
menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} />
|
||||||
|
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
|
||||||
|
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||||
|
<FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
|
||||||
|
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
|
||||||
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
|
||||||
|
</div>
|
||||||
|
</ClientProviders>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ClientLayout }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue