updated service web customer

This commit is contained in:
Berkay 2025-06-03 23:28:55 +03:00
parent 1c0bb741a0
commit 3a35752b46
115 changed files with 5314 additions and 2 deletions

View File

@ -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 });
}

View File

@ -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 });
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" },
}
);
}
}

View File

@ -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" });
}
}

View File

@ -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" });
}
}

View File

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

View File

@ -0,0 +1,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" });
}
}

View File

@ -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" });
}
}

View File

@ -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)
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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} />;
}

View File

@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../styles/custom-scrollbar.css";
@custom-variant dark (&:is(.dark *));

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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';

View File

@ -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>
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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

View File

@ -0,0 +1,13 @@
import { ContentProps } from "@/validations/mutual/dashboard/props";
import ContentToRenderNoPage from "@/pages/mutual/noContent/page";
import { resolveWhichPageToRenderSingle } from "@/pages/resolver/resolver";
const PageToBeChildrendSingle: React.FC<ContentProps> = 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

View File

@ -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>
// )}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -0,0 +1,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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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,
};
};
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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;

View File

@ -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 };

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -0,0 +1,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;

View File

@ -1,10 +1,11 @@
import { ClientRedisOnline } from "@/fetchers/types/context/online/validations";
import { ClientPageConfig } from "@/fetchers/types/context/pageConfig/validations";
import { ClientMenu } from "@/fetchers/types/context/menu/validations";
import { ClientHeader } from "@/fetchers/types/context/header/validations";
import { ClientSelection } from "@/fetchers/types/context/selection/validations";
import { ClientUser } from "@/fetchers/types/context/user/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 { defaultValuesPageConfig } from "@/fetchers/types/context/pageConfig/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";
interface ClientRedisToken {
online: ClientRedisOnline;
online: ClientOnline;
pageConfig: ClientPageConfig;
menu: ClientMenu;
header: ClientHeader;

View File

View File

@ -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 }
}

View File

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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,
};

View File

@ -0,0 +1,9 @@
import { managementAccountTenantMainTr } from "./turkish";
import { managementAccountTenantMainEn } from "./english";
const managementAccountTenantMain = {
tr: managementAccountTenantMainTr,
en: managementAccountTenantMainEn,
}
export { managementAccountTenantMain }

View File

@ -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,
};

View File

@ -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,
};

View File

@ -0,0 +1,9 @@
import { managementAccountTenantMainSecondTr } from "./turkish";
import { managementAccountTenantMainSecondEn } from "./english";
const managementAccountTenantMainSecond = {
tr: managementAccountTenantMainSecondTr,
en: managementAccountTenantMainSecondEn,
};
export { managementAccountTenantMainSecond };

View File

@ -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,
};

View File

@ -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 };

View File

@ -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 };

View File

@ -0,0 +1,8 @@
const contentDefaultEn = {
title: "Content Default",
content: "Content Default",
button: "Content Default",
rows: "Rows",
};
export { contentDefaultEn };

View File

@ -0,0 +1,9 @@
import { contentDefaultTr } from "./turkish";
import { contentDefaultEn } from "./english";
const contentDefault = {
tr: contentDefaultTr,
en: contentDefaultEn,
};
export { contentDefault };

View File

@ -0,0 +1,8 @@
const contentDefaultTr = {
title: "İçerik Varsayılan",
content: "İçerik Varsayılan",
button: "İçerik Varsayılan",
rows: "Satır",
};
export { contentDefaultTr };

View File

@ -0,0 +1,5 @@
const dashboardTranslationEn = {
title: "Dashboard Panel",
};
export { dashboardTranslationEn };

View File

@ -0,0 +1,7 @@
import { dashboardTranslationEn } from "./english";
import { dashboardTranslationTr } from "./turkish";
export const dashboardTranslation = {
en: dashboardTranslationEn,
tr: dashboardTranslationTr,
};

View File

@ -0,0 +1,5 @@
const dashboardTranslationTr = {
title: "Yönetim Panosu",
};
export { dashboardTranslationTr };

View File

@ -0,0 +1,6 @@
const footerDefaultEn = {
description: "Footer Default",
footer: "Footer Info",
};
export { footerDefaultEn };

View File

@ -0,0 +1,6 @@
const footerDefaultTr = {
description: "Footer Bilgi",
footer: "Alt Bilgi",
};
export { footerDefaultTr };

View File

@ -0,0 +1,5 @@
const headerDefaultEn = {
selectedPage: "Selected Page",
};
export { headerDefaultEn };

View File

@ -0,0 +1,5 @@
const headerDefaultTr = {
selectedPage: "Seçili Sayfa",
};
export { headerDefaultTr };

View File

@ -0,0 +1,7 @@
const languageSelectionTranslationEn = {
title: "Language Selection",
english: "English",
turkish: "Turkish",
};
export { languageSelectionTranslationEn };

View File

@ -0,0 +1,7 @@
import { languageSelectionTranslationEn } from "./english";
import { languageSelectionTranslationTr } from "./turkish";
export const languageSelectionTranslation = {
en: languageSelectionTranslationEn,
tr: languageSelectionTranslationTr
}

View File

@ -0,0 +1,7 @@
const languageSelectionTranslationTr = {
title: "Dil Seçimi",
english: "İngilizce",
turkish: "Türkçe",
};
export { languageSelectionTranslationTr };

View File

@ -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 };

View File

@ -0,0 +1,7 @@
import { menuTranslationEn } from "./english";
import { menuTranslationTr } from "./turkish";
export const menuTranslation = {
en: menuTranslationEn,
tr: menuTranslationTr,
};

View File

@ -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 };

View File

@ -0,0 +1,5 @@
const dashboardTranslationEn = {
title: "Dashboard Panel",
};
export { dashboardTranslationEn };

View File

@ -0,0 +1,7 @@
import { dashboardTranslationEn } from "./english";
import { dashboardTranslationTr } from "./turkish";
export const dashboardTranslation = {
en: dashboardTranslationEn,
tr: dashboardTranslationTr,
};

View File

@ -0,0 +1,5 @@
const dashboardTranslationTr = {
title: "Yönetim Panosu",
};
export { dashboardTranslationTr };

View File

View File

@ -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 };

View File

@ -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