updated components common header layouts
This commit is contained in:
@@ -5,8 +5,6 @@ import { CardSkeleton } from "./CardSkeleton";
|
||||
import { getFieldValue, getGridClasses } from "./utils";
|
||||
import { CardDisplayProps } from "./schema";
|
||||
|
||||
// Interface moved to schema.ts
|
||||
|
||||
export function CardDisplay<T>({
|
||||
showFields,
|
||||
data,
|
||||
|
||||
@@ -64,6 +64,7 @@ export function CreateComponent<T>({
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
reset,
|
||||
} = useForm<Record<string, any>>({
|
||||
defaultValues,
|
||||
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
||||
@@ -71,6 +72,33 @@ export function CreateComponent<T>({
|
||||
|
||||
const formValues = watch();
|
||||
|
||||
// Get language-specific validation schema if available
|
||||
useEffect(() => {
|
||||
if (formProps.schemaPath) {
|
||||
const loadLanguageValidationSchema = async () => {
|
||||
try {
|
||||
// Dynamic import of the schema module
|
||||
const schemaModule = await import(formProps.schemaPath);
|
||||
|
||||
// Check if language-specific schema functions are available
|
||||
if (schemaModule.getCreateApplicationSchema) {
|
||||
const langValidationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
||||
|
||||
// Reset the form with the current values
|
||||
reset(defaultValues);
|
||||
|
||||
// Update the validation schema in formProps for future validations
|
||||
formProps.validationSchema = langValidationSchema;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading language-specific validation schema:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadLanguageValidationSchema();
|
||||
}
|
||||
}, [lang, formProps.schemaPath, reset, defaultValues]);
|
||||
|
||||
// Handle form submission
|
||||
const onSubmit: SubmitHandler<Record<string, any>> = async (data) => {
|
||||
try {
|
||||
@@ -112,23 +140,17 @@ export function CreateComponent<T>({
|
||||
|
||||
// Translate group names for display dynamically
|
||||
const getGroupTitle = (groupName: string) => {
|
||||
// First check if there's a translation for the exact group key
|
||||
// Check if we have a translation for this group name
|
||||
if (t[groupName]) {
|
||||
return t[groupName];
|
||||
}
|
||||
|
||||
// Try to format the group name in a more readable way if no translation exists
|
||||
// Convert camelCase or snake_case to Title Case with spaces
|
||||
// If no translation is found, just format the name as a fallback
|
||||
const formattedName = groupName
|
||||
// Insert space before capital letters and uppercase the first letter
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
// Replace underscores with spaces
|
||||
.replace(/_/g, ' ')
|
||||
// Capitalize first letter
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
// Capitalize each word
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
return formattedName;
|
||||
};
|
||||
|
||||
@@ -230,9 +252,17 @@ export function CreateComponent<T>({
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit" className="w-full">
|
||||
{t.save || "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
<Card className="w-full mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>{t.create || "Create"}</CardTitle>
|
||||
@@ -265,14 +295,7 @@ export function CreateComponent<T>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-6">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{t.save || "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
@@ -19,9 +19,9 @@ export function FormDisplay<T>({
|
||||
}: FormDisplayProps<T>) {
|
||||
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
||||
|
||||
// Dynamically import schema definitions if provided in formProps
|
||||
// Update form props when language or mode changes
|
||||
useEffect(() => {
|
||||
const loadSchemaDefinitions = async () => {
|
||||
const updateFormProps = async () => {
|
||||
try {
|
||||
// Check if schemaPath is provided in formProps
|
||||
if (formProps.schemaPath) {
|
||||
@@ -40,12 +40,14 @@ export function FormDisplay<T>({
|
||||
fieldDefs = schemaModule.viewFieldDefinitions;
|
||||
}
|
||||
|
||||
// Get the appropriate validation schema based on mode
|
||||
// Get the appropriate validation schema based on mode and language
|
||||
let validationSchema;
|
||||
if (mode === "create" && schemaModule.CreateApplicationSchema) {
|
||||
validationSchema = schemaModule.CreateApplicationSchema;
|
||||
} else if (mode === "update" && schemaModule.UpdateApplicationSchema) {
|
||||
validationSchema = schemaModule.UpdateApplicationSchema;
|
||||
if (mode === "create" && schemaModule.getCreateApplicationSchema) {
|
||||
// Use language-aware schema factory function
|
||||
validationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
||||
} else if (mode === "update" && schemaModule.getUpdateApplicationSchema) {
|
||||
// Use language-aware schema factory function
|
||||
validationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
||||
} else if (mode === "view" && schemaModule.ViewApplicationSchema) {
|
||||
validationSchema = schemaModule.ViewApplicationSchema;
|
||||
} else if (schemaModule.ApplicationSchema) {
|
||||
@@ -55,22 +57,40 @@ export function FormDisplay<T>({
|
||||
// Get the grouped field definitions structure if available
|
||||
const groupedFieldDefs = schemaModule.baseFieldDefinitions || {};
|
||||
|
||||
// Update form props with schema information
|
||||
// Update form props with schema information and current language
|
||||
setEnhancedFormProps({
|
||||
...formProps,
|
||||
fieldDefinitions: fieldDefs || {},
|
||||
validationSchema,
|
||||
fieldsByMode: schemaModule.fieldsByMode || {},
|
||||
groupedFieldDefinitions: groupedFieldDefs,
|
||||
// Add current language to force child components to recognize changes
|
||||
currentLang: lang,
|
||||
// Add schema path for dynamic imports in child components
|
||||
schemaPath: formProps.schemaPath
|
||||
});
|
||||
} else {
|
||||
// If no schema path, just update with current language
|
||||
setEnhancedFormProps({
|
||||
...formProps,
|
||||
currentLang: lang
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading schema definitions:", error);
|
||||
// Even on error, update the language
|
||||
setEnhancedFormProps({
|
||||
...formProps,
|
||||
currentLang: lang
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadSchemaDefinitions();
|
||||
}, [formProps, mode, lang]); // Added lang as a dependency to ensure re-fetch when language changes
|
||||
updateFormProps();
|
||||
}, [formProps, mode, lang]); // Lang dependency ensures re-fetch when language changes
|
||||
|
||||
// Debug the props received by FormDisplay
|
||||
// FormDisplay component renders different form modes based on the mode prop
|
||||
|
||||
// Render the appropriate component based on the mode
|
||||
switch (mode) {
|
||||
@@ -89,9 +109,12 @@ export function FormDisplay<T>({
|
||||
/>
|
||||
);
|
||||
case "update":
|
||||
// Create a stable key for the component to ensure proper re-rendering
|
||||
const updateKey = `update-${lang}-${(initialData as any)?.uu_id || 'new'}`;
|
||||
|
||||
return initialData ? (
|
||||
<UpdateComponent<T>
|
||||
key={`update-${lang}`} // Add key with lang to force re-render on language change
|
||||
key={updateKey} // Add key with lang and item ID to force re-render
|
||||
initialData={initialData}
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { UpdateComponentProps, FieldDefinition } from "./types";
|
||||
@@ -26,29 +26,48 @@ export function UpdateComponent<T>({
|
||||
}: UpdateComponentProps<T>) {
|
||||
const t = translations[lang as keyof typeof translations] || {};
|
||||
|
||||
// Get field definitions from formProps if available
|
||||
const fieldDefinitions = formProps.fieldDefinitions || {};
|
||||
const validationSchema = formProps.validationSchema;
|
||||
|
||||
// Ensure field definitions are processed only once
|
||||
const processedFieldDefinitions = useMemo(() => {
|
||||
const processed = { ...fieldDefinitions };
|
||||
// Make all fields editable except system fields
|
||||
Object.entries(processed).forEach(([fieldName, definition]) => {
|
||||
if (fieldName !== 'uu_id' && fieldName !== 'created_at' && fieldName !== 'updated_at') {
|
||||
(processed[fieldName] as FieldDefinition).readOnly = false;
|
||||
}
|
||||
});
|
||||
return processed;
|
||||
}, [fieldDefinitions]);
|
||||
|
||||
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(fieldDefinitions).length > 0) {
|
||||
if (Object.keys(processedFieldDefinitions).length > 0) {
|
||||
const groups: Record<string, FieldDefinition[]> = {};
|
||||
|
||||
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
||||
// Group the processed field definitions
|
||||
Object.entries(processedFieldDefinitions).forEach(([fieldName, definition]) => {
|
||||
// Convert to FieldDefinition type
|
||||
const def = definition as FieldDefinition;
|
||||
|
||||
// Add the field name to the definition
|
||||
const fieldDef = { ...def, name: fieldName };
|
||||
|
||||
// Add to the appropriate group
|
||||
if (!groups[def.group]) {
|
||||
groups[def.group] = [];
|
||||
}
|
||||
groups[def.group].push({ ...def, name: fieldName });
|
||||
groups[def.group].push(fieldDef);
|
||||
});
|
||||
|
||||
setGroupedFields(groups);
|
||||
}
|
||||
}, [fieldDefinitions]);
|
||||
}, [processedFieldDefinitions]);
|
||||
|
||||
const defaultValues: Record<string, any> = {};
|
||||
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
||||
Object.entries(processedFieldDefinitions).forEach(([key, def]) => {
|
||||
const fieldDef = def as FieldDefinition;
|
||||
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
||||
});
|
||||
@@ -57,6 +76,9 @@ export function UpdateComponent<T>({
|
||||
Object.assign(defaultValues, initialData as Record<string, any>);
|
||||
}
|
||||
|
||||
// Track the current language to detect changes
|
||||
const [currentLang, setCurrentLang] = useState(lang);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -83,6 +105,53 @@ export function UpdateComponent<T>({
|
||||
}
|
||||
}, [initialData, reset]);
|
||||
|
||||
// Detect language changes and update validation schema
|
||||
useEffect(() => {
|
||||
// If language has changed, update the form
|
||||
if (currentLang !== lang || formProps.currentLang !== lang) {
|
||||
const updateValidationForLanguage = async () => {
|
||||
try {
|
||||
// If we have a schema path, dynamically load the schema for the current language
|
||||
if (formProps.schemaPath) {
|
||||
// Dynamic import of the schema module
|
||||
const schemaModule = await import(formProps.schemaPath);
|
||||
|
||||
// Check if language-specific schema functions are available
|
||||
if (schemaModule.getUpdateApplicationSchema) {
|
||||
// Get the schema for the current language
|
||||
const langValidationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
||||
|
||||
// Save current form values
|
||||
const formValues = watch();
|
||||
|
||||
// Reset the form with current values but clear errors
|
||||
reset(formValues, {
|
||||
keepDirty: true,
|
||||
keepValues: true,
|
||||
keepErrors: false
|
||||
});
|
||||
|
||||
// Manually trigger validation after reset
|
||||
setTimeout(() => {
|
||||
// Trigger validation for all fields to show updated error messages
|
||||
Object.keys(formValues).forEach(fieldName => {
|
||||
trigger(fieldName);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
// Update our tracked language
|
||||
setCurrentLang(lang);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating validation schema for language:", error);
|
||||
}
|
||||
};
|
||||
|
||||
updateValidationForLanguage();
|
||||
}
|
||||
}, [lang, formProps.currentLang, currentLang, formProps.schemaPath, reset, watch, trigger]);
|
||||
|
||||
const formValues = watch();
|
||||
|
||||
// Handle form submission
|
||||
@@ -149,10 +218,12 @@ export function UpdateComponent<T>({
|
||||
|
||||
// Translate group names for display dynamically
|
||||
const getGroupTitle = (groupName: string) => {
|
||||
// Check if we have a translation for this group name
|
||||
if (t[groupName]) {
|
||||
return t[groupName];
|
||||
}
|
||||
|
||||
// If no translation is found, just format the name as a fallback
|
||||
const formattedName = groupName
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/_/g, ' ')
|
||||
@@ -289,6 +360,7 @@ export function UpdateComponent<T>({
|
||||
<CardDescription>{t.updateDescription || "Update existing item"}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
{/* Display validation errors summary if any */}
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
@@ -306,6 +378,15 @@ export function UpdateComponent<T>({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit" className="w-full">
|
||||
{t.save || "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Render fields grouped by their group property */}
|
||||
<div className="space-y-6">
|
||||
{Object.entries(groupedFields).map(([groupName, fields]) => (
|
||||
@@ -322,14 +403,7 @@ export function UpdateComponent<T>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-6">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
{t.cancel || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{t.save || "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
export type Language = "en" | "tr";
|
||||
|
||||
interface LanguageSelectionComponentProps {
|
||||
lang: Language;
|
||||
setLang: (lang: Language) => void;
|
||||
translations?: Record<string, any>;
|
||||
className?: string;
|
||||
}
|
||||
import { Language, LanguageSelectionComponentProps } from "@/components/common/schemas";
|
||||
|
||||
export const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({
|
||||
lang,
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import { PageProps } from "@/validations/translations/translation";
|
||||
import React, { ReactElement } from "react";
|
||||
import React from "react";
|
||||
|
||||
export interface DashboardPageParams {
|
||||
/**
|
||||
* The active page path, e.g., "/application", "/dashboard"
|
||||
*/
|
||||
pageUrl: string;
|
||||
|
||||
/**
|
||||
* The search parameters from Next.js
|
||||
*/
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}
|
||||
|
||||
export interface DashboardPageResult {
|
||||
/**
|
||||
* The active page path
|
||||
*/
|
||||
activePage: string;
|
||||
|
||||
/**
|
||||
* The resolved search parameters
|
||||
*/
|
||||
searchParamsInstance: { [key: string]: string | undefined };
|
||||
|
||||
/**
|
||||
* The current language, either from search params or default
|
||||
*/
|
||||
lang: "en" | "tr";
|
||||
|
||||
/**
|
||||
* The page component to render
|
||||
*/
|
||||
PageComponent: React.FC<PageProps>;
|
||||
}
|
||||
|
||||
@@ -50,38 +28,29 @@ export async function useDashboardPage({
|
||||
}: DashboardPageParams): Promise<DashboardPageResult> {
|
||||
let searchParamsInstance: { [key: string]: string | undefined } = {};
|
||||
const defaultLang = "en";
|
||||
// Validate pageUrl
|
||||
|
||||
if (!pageUrl || typeof pageUrl !== "string") {
|
||||
throw new Error(`Invalid page URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
// Resolve search params
|
||||
try {
|
||||
searchParamsInstance = await searchParams;
|
||||
} catch (err) {
|
||||
console.error("Error resolving search parameters:", err);
|
||||
// Still throw the error to be caught by Next.js error boundary
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Determine language
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
|
||||
|
||||
// Validate language
|
||||
if (lang !== "en" && lang !== "tr") {
|
||||
console.warn(
|
||||
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
|
||||
);
|
||||
}
|
||||
|
||||
// Get page component
|
||||
const PageComponent = retrievePageByUrl(pageUrl);
|
||||
|
||||
// Check if page component exists
|
||||
const PageComponent = retrievePageByUrl(pageUrl, lang);
|
||||
if (!PageComponent) {
|
||||
throw new Error(`Page component not found for URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
return {
|
||||
activePage: pageUrl,
|
||||
searchParamsInstance,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// Carried schemas from any request and response
|
||||
|
||||
// Common request parameters interface
|
||||
@@ -34,4 +33,13 @@ export interface PagePagination {
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
export type Language = "en" | "tr";
|
||||
|
||||
export interface LanguageSelectionComponentProps {
|
||||
lang: Language;
|
||||
setLang: (lang: Language) => void;
|
||||
translations?: Record<string, any>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
|
||||
import { logoutActiveSession } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LanguageSelectionComponent } from "../common/HeaderSelections/LanguageSelectionComponent";
|
||||
|
||||
interface HeaderProps {
|
||||
lang: "en" | "tr";
|
||||
setLang: (lang: "en" | "tr") => void;
|
||||
}
|
||||
|
||||
// Language dictionary for the dropdown menu
|
||||
@@ -96,7 +98,7 @@ const mockMessages = [
|
||||
},
|
||||
];
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||
const Header: React.FC<HeaderProps> = ({ lang, setLang }) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
|
||||
const [isMessagesOpen, setIsMessagesOpen] = useState(false);
|
||||
@@ -185,220 +187,231 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-full">
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center w-full">
|
||||
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
|
||||
{/* Notifications dropdown */}
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsNotificationsOpen(!isNotificationsOpen);
|
||||
setIsMessagesOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Bell size={20} className="text-gray-600" />
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{notifications.filter((n) => !n.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications dropdown menu */}
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.notifications}</h3>
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<button
|
||||
onClick={markAllNotificationsAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noNotifications}
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{!notification.read && (
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formatDate(notification.time)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/notifications"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
{/* Notifications dropdown */}
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsNotificationsOpen(!isNotificationsOpen);
|
||||
setIsMessagesOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Bell size={20} className="text-gray-600" />
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{notifications.filter((n) => !n.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown */}
|
||||
<div className="relative" ref={messagesRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={20} className="text-gray-600" />
|
||||
{messages.some((m) => !m.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{messages.filter((m) => !m.read).length}
|
||||
</span>
|
||||
{/* Notifications dropdown menu */}
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.notifications}</h3>
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<button
|
||||
onClick={markAllNotificationsAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noNotifications}
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{!notification.read && (
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formatDate(notification.time)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/notifications"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown menu */}
|
||||
{isMessagesOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.messages}</h3>
|
||||
{messages.some((m) => !m.read) && (
|
||||
<button
|
||||
onClick={markAllMessagesAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* Messages dropdown */}
|
||||
<div className="relative" ref={messagesRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={20} className="text-gray-600" />
|
||||
{messages.some((m) => !m.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{messages.filter((m) => !m.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noMessages}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
{/* Messages dropdown menu */}
|
||||
{isMessagesOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.messages}</h3>
|
||||
{messages.some((m) => !m.read) && (
|
||||
<button
|
||||
onClick={markAllMessagesAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-xs font-semibold">
|
||||
{message.avatar}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{message.sender}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(message.time)}
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noMessages}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-xs font-semibold">
|
||||
{message.avatar}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{message.sender}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(message.time)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/messages"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/messages"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<User size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<ChevronDown size={16} className="ml-1 text-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<a
|
||||
href="/profile"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<User size={16} className="mr-2" />
|
||||
{t.profile}
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t.settings}
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
{t.logout}
|
||||
</button>
|
||||
{/* Language selection */}
|
||||
<div className="mr-4">
|
||||
<LanguageSelectionComponent
|
||||
lang={lang}
|
||||
setLang={setLang}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<User size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<ChevronDown size={16} className="ml-1 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<a
|
||||
href="/profile"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<User size={16} className="mr-2" />
|
||||
{t.profile}
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t.settings}
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
{t.logout}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
"use client";
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { useState, useEffect, ReactNode } from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { DashboardLayoutProps } from "./schema";
|
||||
import { Language } from "@/components/common/schemas";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
// Page Content component to wrap the children
|
||||
interface PageContentProps {
|
||||
children: ReactNode;
|
||||
lang: "en" | "tr";
|
||||
activePage: string;
|
||||
siteUrls: string[];
|
||||
lang: Language;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable dashboard layout component that provides consistent structure
|
||||
* for all dashboard pages with sidebar, header, and content area.
|
||||
*/
|
||||
const PageContent: React.FC<PageContentProps> = ({ children, lang }) => {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
{React.cloneElement(children as React.ReactElement<any>, { lang })}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
||||
children,
|
||||
lang,
|
||||
activePage,
|
||||
siteUrls,
|
||||
}) => {
|
||||
const [language, setLanguage] = useState<Language>(lang as Language);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||
<ClientMenu siteUrls={siteUrls} lang={lang} activePage={activePage} />
|
||||
<ClientMenu lang={language} activePage={activePage} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Header Component */}
|
||||
<Header lang={lang} />
|
||||
|
||||
{/* Header Component - Either custom or default */}
|
||||
<Header lang={language} setLang={setLanguage} />
|
||||
{/* Page Content */}
|
||||
<div className="container mx-auto p-4">
|
||||
{children}
|
||||
</div>
|
||||
<PageContent lang={language}>{children}</PageContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,10 +11,10 @@ interface PageTemplateProps {
|
||||
title: string;
|
||||
lang: "en" | "tr";
|
||||
translations: Record<string, any>;
|
||||
|
||||
|
||||
// Search section
|
||||
searchSection?: ReactNode;
|
||||
|
||||
|
||||
// Data and pagination
|
||||
data: any[];
|
||||
pagination: any;
|
||||
@@ -22,20 +22,20 @@ interface PageTemplateProps {
|
||||
loading: boolean;
|
||||
error: any;
|
||||
refetch: () => void;
|
||||
|
||||
|
||||
// Content display
|
||||
contentDisplay: ReactNode;
|
||||
|
||||
|
||||
// Form handling
|
||||
formComponent?: ReactNode;
|
||||
mode: FormMode;
|
||||
setMode: (mode: FormMode) => void;
|
||||
handleCreateClick: () => void;
|
||||
handleCancel: () => void;
|
||||
|
||||
|
||||
// Language handling
|
||||
setLang?: (lang: Language) => void;
|
||||
|
||||
|
||||
// Optional components
|
||||
headerActions?: ReactNode;
|
||||
additionalActions?: ReactNode;
|
||||
@@ -77,18 +77,18 @@ export const PageTemplate: React.FC<PageTemplateProps> = ({
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<div className="flex space-x-4">
|
||||
{/* Grid Selection */}
|
||||
<GridSelectionComponent
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
<GridSelectionComponent
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
/>
|
||||
|
||||
|
||||
{/* Language Selection */}
|
||||
<LanguageSelectionComponent
|
||||
lang={lang as Language}
|
||||
<LanguageSelectionComponent
|
||||
lang={lang as Language}
|
||||
translations={translations}
|
||||
setLang={setLang || (() => { })}
|
||||
setLang={setLang || (() => {})}
|
||||
/>
|
||||
|
||||
|
||||
{/* Additional header actions */}
|
||||
{headerActions}
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@ export const PageTemplate: React.FC<PageTemplateProps> = ({
|
||||
formComponent || (
|
||||
<div className="p-4 bg-gray-100 rounded-md">
|
||||
<p>Form component not provided</p>
|
||||
<button
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="mt-4 px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface DashboardLayoutProps {
|
||||
children: ReactNode;
|
||||
lang: "en" | "tr";
|
||||
activePage: string;
|
||||
}
|
||||
@@ -8,7 +8,12 @@ async function DashboardPage({
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||
const {
|
||||
activePage,
|
||||
searchParamsInstance,
|
||||
lang,
|
||||
PageComponent,
|
||||
} = await useDashboardPage({
|
||||
pageUrl: "/application",
|
||||
searchParams,
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@ import { CardSkeleton } from "./CardSkeleton";
|
||||
import { getFieldValue, getGridClasses } from "./utils";
|
||||
import { CardDisplayProps } from "./schema";
|
||||
|
||||
// Interface moved to schema.ts
|
||||
|
||||
export function CardDisplay<T>({
|
||||
showFields,
|
||||
data,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
export type Language = "en" | "tr";
|
||||
|
||||
interface LanguageSelectionComponentProps {
|
||||
lang: Language;
|
||||
setLang: (lang: Language) => void;
|
||||
translations?: Record<string, any>;
|
||||
className?: string;
|
||||
}
|
||||
import { Language, LanguageSelectionComponentProps } from "@/components/common/schemas";
|
||||
|
||||
export const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({
|
||||
lang,
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import { PageProps } from "@/validations/translations/translation";
|
||||
import React, { ReactElement } from "react";
|
||||
import React from "react";
|
||||
|
||||
export interface DashboardPageParams {
|
||||
/**
|
||||
* The active page path, e.g., "/application", "/dashboard"
|
||||
*/
|
||||
pageUrl: string;
|
||||
|
||||
/**
|
||||
* The search parameters from Next.js
|
||||
*/
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}
|
||||
|
||||
export interface DashboardPageResult {
|
||||
/**
|
||||
* The active page path
|
||||
*/
|
||||
activePage: string;
|
||||
|
||||
/**
|
||||
* The resolved search parameters
|
||||
*/
|
||||
searchParamsInstance: { [key: string]: string | undefined };
|
||||
|
||||
/**
|
||||
* The current language, either from search params or default
|
||||
*/
|
||||
lang: "en" | "tr";
|
||||
|
||||
/**
|
||||
* The page component to render
|
||||
*/
|
||||
PageComponent: React.FC<PageProps>;
|
||||
}
|
||||
|
||||
@@ -50,38 +28,29 @@ export async function useDashboardPage({
|
||||
}: DashboardPageParams): Promise<DashboardPageResult> {
|
||||
let searchParamsInstance: { [key: string]: string | undefined } = {};
|
||||
const defaultLang = "en";
|
||||
// Validate pageUrl
|
||||
|
||||
if (!pageUrl || typeof pageUrl !== "string") {
|
||||
throw new Error(`Invalid page URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
// Resolve search params
|
||||
try {
|
||||
searchParamsInstance = await searchParams;
|
||||
} catch (err) {
|
||||
console.error("Error resolving search parameters:", err);
|
||||
// Still throw the error to be caught by Next.js error boundary
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Determine language
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
|
||||
|
||||
// Validate language
|
||||
if (lang !== "en" && lang !== "tr") {
|
||||
console.warn(
|
||||
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
|
||||
);
|
||||
}
|
||||
|
||||
// Get page component
|
||||
const PageComponent = retrievePageByUrl(pageUrl);
|
||||
|
||||
// Check if page component exists
|
||||
const PageComponent = retrievePageByUrl(pageUrl, lang);
|
||||
if (!PageComponent) {
|
||||
throw new Error(`Page component not found for URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
return {
|
||||
activePage: pageUrl,
|
||||
searchParamsInstance,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// Carried schemas from any request and response
|
||||
|
||||
// Common request parameters interface
|
||||
@@ -34,4 +33,13 @@ export interface PagePagination {
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
export type Language = "en" | "tr";
|
||||
|
||||
export interface LanguageSelectionComponentProps {
|
||||
lang: Language;
|
||||
setLang: (lang: Language) => void;
|
||||
translations?: Record<string, any>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
|
||||
import { logoutActiveSession } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LanguageSelectionComponent } from "../common/HeaderSelections/LanguageSelectionComponent";
|
||||
|
||||
interface HeaderProps {
|
||||
lang: "en" | "tr";
|
||||
setLang: (lang: "en" | "tr") => void;
|
||||
}
|
||||
|
||||
// Language dictionary for the dropdown menu
|
||||
@@ -96,7 +98,7 @@ const mockMessages = [
|
||||
},
|
||||
];
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||
const Header: React.FC<HeaderProps> = ({ lang, setLang }) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
|
||||
const [isMessagesOpen, setIsMessagesOpen] = useState(false);
|
||||
@@ -185,222 +187,231 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-full">
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center w-full">
|
||||
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
|
||||
{/* Notifications dropdown */}
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsNotificationsOpen(!isNotificationsOpen);
|
||||
setIsMessagesOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Bell size={20} className="text-gray-600" />
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{notifications.filter((n) => !n.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications dropdown menu */}
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.notifications}</h3>
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<button
|
||||
onClick={markAllNotificationsAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noNotifications}
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{!notification.read && (
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formatDate(notification.time)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/notifications"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
{/* Notifications dropdown */}
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsNotificationsOpen(!isNotificationsOpen);
|
||||
setIsMessagesOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Bell size={20} className="text-gray-600" />
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{notifications.filter((n) => !n.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown */}
|
||||
<div className="relative" ref={messagesRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={20} className="text-gray-600" />
|
||||
{messages.some((m) => !m.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{messages.filter((m) => !m.read).length}
|
||||
</span>
|
||||
{/* Notifications dropdown menu */}
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.notifications}</h3>
|
||||
{notifications.some((n) => !n.read) && (
|
||||
<button
|
||||
onClick={markAllNotificationsAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noNotifications}
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{notification.title}
|
||||
</h4>
|
||||
{!notification.read && (
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formatDate(notification.time)}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/notifications"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Messages dropdown menu */}
|
||||
{isMessagesOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.messages}</h3>
|
||||
{messages.some((m) => !m.read) && (
|
||||
<button
|
||||
onClick={markAllMessagesAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* Messages dropdown */}
|
||||
<div className="relative" ref={messagesRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer relative"
|
||||
onClick={() => {
|
||||
setIsMessagesOpen(!isMessagesOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<MessageSquare size={20} className="text-gray-600" />
|
||||
{messages.some((m) => !m.read) && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{messages.filter((m) => !m.read).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noMessages}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
{/* Messages dropdown menu */}
|
||||
{isMessagesOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<div className="px-4 py-2 border-b flex justify-between items-center">
|
||||
<h3 className="font-semibold">{t.messages}</h3>
|
||||
{messages.some((m) => !m.read) && (
|
||||
<button
|
||||
onClick={markAllMessagesAsRead}
|
||||
className="text-xs text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-xs font-semibold">
|
||||
{message.avatar}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{message.sender}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(message.time)}
|
||||
{t.markAllAsRead}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{messages.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
{t.noMessages}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||
<span className="text-xs font-semibold">
|
||||
{message.avatar}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{message.sender}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(message.time)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{message.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/messages"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
<div className="px-4 py-2 border-t text-center">
|
||||
<a
|
||||
href="/messages"
|
||||
className="text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{t.viewAll}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<User size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<ChevronDown size={16} className="ml-1 text-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<a
|
||||
href="/profile"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<User size={16} className="mr-2" />
|
||||
{t.profile}
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t.settings}
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
{t.logout}
|
||||
</button>
|
||||
{/* Language selection */}
|
||||
<div className="mr-4">
|
||||
<LanguageSelectionComponent
|
||||
lang={lang}
|
||||
setLang={setLang}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profile dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
setIsNotificationsOpen(false);
|
||||
setIsMessagesOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<User size={20} className="text-gray-600" />
|
||||
</div>
|
||||
<ChevronDown size={16} className="ml-1 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
|
||||
<a
|
||||
href="/profile"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<User size={16} className="mr-2" />
|
||||
{t.profile}
|
||||
</a>
|
||||
<a
|
||||
href="/settings"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t.settings}
|
||||
</a>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
<LogOut size={16} className="mr-2" />
|
||||
{t.logout}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,69 +1,44 @@
|
||||
"use client";
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { useState, useEffect, ReactNode } from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { DashboardLayoutProps } from "./schema";
|
||||
import { Language } from "@/components/common/schemas";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
// Page Content component to wrap the children
|
||||
interface PageContentProps {
|
||||
children: ReactNode;
|
||||
lang: "en" | "tr";
|
||||
activePage: string;
|
||||
|
||||
// Optional props for client-frontend application
|
||||
sidebarContent?: ReactNode;
|
||||
customHeader?: ReactNode;
|
||||
pageInfo?: Record<string, string>;
|
||||
searchPlaceholder?: Record<string, string>;
|
||||
lang: Language;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable dashboard layout component that provides consistent structure
|
||||
* for all dashboard pages with sidebar, header, and content area.
|
||||
*/
|
||||
const PageContent: React.FC<PageContentProps> = ({ children, lang }) => {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
{React.cloneElement(children as React.ReactElement<any>, { lang })}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
|
||||
children,
|
||||
lang,
|
||||
activePage,
|
||||
sidebarContent,
|
||||
customHeader,
|
||||
pageInfo,
|
||||
searchPlaceholder,
|
||||
}) => {
|
||||
const [language, setLanguage] = useState<Language>(lang as Language);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-y-auto">
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||
{sidebarContent ? (
|
||||
sidebarContent
|
||||
) : (
|
||||
<ClientMenu lang={lang} activePage={activePage} />
|
||||
)}
|
||||
<ClientMenu lang={language} activePage={activePage} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Header Component - Either custom or default */}
|
||||
{customHeader ? (
|
||||
customHeader
|
||||
) : pageInfo && searchPlaceholder ? (
|
||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold">{pageInfo[lang]}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder[lang]}
|
||||
className="border px-3 py-2 rounded-lg"
|
||||
/>
|
||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
||||
</div>
|
||||
</header>
|
||||
) : (
|
||||
<Header lang={lang} />
|
||||
)}
|
||||
|
||||
<Header lang={language} setLang={setLanguage} />
|
||||
{/* Page Content */}
|
||||
<div className={`${customHeader ? 'p-4 overflow-y-auto' : 'container mx-auto p-4'}`}>
|
||||
{children}
|
||||
</div>
|
||||
<PageContent lang={language}>{children}</PageContent>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface DashboardLayoutProps {
|
||||
children: ReactNode;
|
||||
lang: "en" | "tr";
|
||||
activePage: string;
|
||||
}
|
||||
@@ -1,53 +1,32 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const NavigationLanguage = {
|
||||
en: {
|
||||
"/dashboard": "Dashboard",
|
||||
"/append/event": "Event Append",
|
||||
"/append/service": "Service Append",
|
||||
"/application": "Application",
|
||||
"/employee": "Employee",
|
||||
"/ocuppant": "Ocuppant",
|
||||
},
|
||||
tr: {
|
||||
"/dashboard": "Kontrol Paneli",
|
||||
"/append/event": "Event Append",
|
||||
"/append/service": "Service Append",
|
||||
"/application": "Application",
|
||||
"/employee": "Employee",
|
||||
"/ocuppant": "Ocuppant",
|
||||
},
|
||||
};
|
||||
import React, { JSX } from "react";
|
||||
import { getNavigationMenu } from "./type";
|
||||
|
||||
function NavigationMenu({
|
||||
lang,
|
||||
activePage,
|
||||
}: {
|
||||
lang: string;
|
||||
lang: "en" | "tr";
|
||||
activePage: string;
|
||||
}) {
|
||||
// Get the navigation items based on the selected language
|
||||
const navItems =
|
||||
NavigationLanguage[lang as keyof typeof NavigationLanguage] ||
|
||||
NavigationLanguage.en;
|
||||
|
||||
const navItems = getNavigationMenu(lang);
|
||||
|
||||
function createLinkComponent(url: string, title: string): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
key={url}
|
||||
href={url}
|
||||
className={`px-4 py-2 rounded-md transition-colors duration-200 ${url === activePage ? "bg-emerald-500 text-white" : "bg-white hover:bg-gray-100"}`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<nav className="flex flex-col space-y-2 p-4">
|
||||
{Object.entries(navItems).map(([url, title]) => (
|
||||
<Link
|
||||
key={url}
|
||||
href={url}
|
||||
className={`px-4 py-2 rounded-md transition-colors duration-200 ${
|
||||
url === activePage
|
||||
? "bg-emerald-500 text-white"
|
||||
: "bg-white hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
))}
|
||||
{Object.entries(navItems).map(([url, title]) => createLinkComponent(url, title))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Menu from "./store";
|
||||
|
||||
// Define TypeScript interfaces for menu structure
|
||||
export interface LanguageTranslation {
|
||||
tr: string;
|
||||
en: string;
|
||||
}
|
||||
|
||||
export interface MenuThirdLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface MenuSecondLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: MenuThirdLevel[];
|
||||
}
|
||||
|
||||
export interface MenuFirstLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: MenuSecondLevel[];
|
||||
}
|
||||
|
||||
// Define interfaces for the filtered menu structure
|
||||
export interface FilteredMenuThirdLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
export interface FilteredMenuSecondLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: FilteredMenuThirdLevel[];
|
||||
}
|
||||
|
||||
export interface FilteredMenuFirstLevel {
|
||||
name: string;
|
||||
lg: LanguageTranslation;
|
||||
subList: FilteredMenuSecondLevel[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the menu structure based on intersections with provided URLs
|
||||
* @param {string[]} siteUrls - Array of site URLs to check for intersection
|
||||
* @returns {Array} - Filtered menu structure with only matching items
|
||||
*/
|
||||
export function transformMenu(siteUrls: string[]) {
|
||||
// Process the menu structure
|
||||
const filteredMenu: FilteredMenuFirstLevel[] = Menu.reduce(
|
||||
(acc: FilteredMenuFirstLevel[], firstLevel: MenuFirstLevel) => {
|
||||
// Create a new first level item with empty subList
|
||||
const newFirstLevel: FilteredMenuFirstLevel = {
|
||||
name: firstLevel.name,
|
||||
lg: { ...firstLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process second level items
|
||||
firstLevel.subList.forEach((secondLevel: MenuSecondLevel) => {
|
||||
// Create a new second level item with empty subList
|
||||
const newSecondLevel: FilteredMenuSecondLevel = {
|
||||
name: secondLevel.name,
|
||||
lg: { ...secondLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process third level items
|
||||
secondLevel.subList.forEach((thirdLevel: MenuThirdLevel) => {
|
||||
// Check if the third level's siteUrl matches exactly
|
||||
if (
|
||||
thirdLevel.siteUrl &&
|
||||
siteUrls.some((url) => url === thirdLevel.siteUrl)
|
||||
) {
|
||||
// Create a modified third level item
|
||||
const newThirdLevel: FilteredMenuThirdLevel = {
|
||||
name: thirdLevel.name,
|
||||
lg: { ...thirdLevel.lg },
|
||||
siteUrl: thirdLevel.siteUrl,
|
||||
};
|
||||
|
||||
// Add the modified third level to the second level's subList
|
||||
newSecondLevel.subList.push(newThirdLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the second level to the first level if it has any matching third level items
|
||||
if (newSecondLevel.subList.length > 0) {
|
||||
newFirstLevel.subList.push(newSecondLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the first level to the result if it has any matching second level items
|
||||
if (newFirstLevel.subList.length > 0) {
|
||||
acc.push(newFirstLevel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return filteredMenu;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, Suspense } from "react";
|
||||
import React, { useEffect, useState, Suspense, JSX } from "react";
|
||||
import { retrieveUserSelection } from "@/apicalls/cookies/token";
|
||||
import EmployeeProfileSection from "./EmployeeProfileSection";
|
||||
import OccupantProfileSection from "./OccupantProfileSection";
|
||||
@@ -23,18 +23,13 @@ const dashboardLanguage = {
|
||||
},
|
||||
};
|
||||
|
||||
const ClientMenu: React.FC<ClientMenuProps> = ({ lang = "en", activePage }) => {
|
||||
const t =
|
||||
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
|
||||
dashboardLanguage.en;
|
||||
const ClientMenu: React.FC<ClientMenuProps> = ({ lang, activePage }) => {
|
||||
const t = dashboardLanguage[lang as keyof typeof dashboardLanguage] || dashboardLanguage.en;
|
||||
|
||||
// State for loading indicator, user type, and user selection data
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [userType, setUserType] = useState<string | null>(null);
|
||||
const [userSelectionData, setUserSelectionData] =
|
||||
useState<UserSelection | null>(null);
|
||||
const [userSelectionData, setUserSelectionData] = useState<UserSelection | null>(null);
|
||||
|
||||
// Fetch user selection data
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -52,6 +47,21 @@ const ClientMenu: React.FC<ClientMenuProps> = ({ lang = "en", activePage }) => {
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
function createProfileComponent(): JSX.Element {
|
||||
return (
|
||||
loading ? (
|
||||
<ProfileLoadingState loadingText={t.loading} />
|
||||
) : userType === "employee" && userSelectionData ? (
|
||||
<EmployeeProfileSection userSelectionData={userSelectionData} lang={lang as "en" | "tr"} />
|
||||
) : userType === "occupant" && userSelectionData ? (
|
||||
<OccupantProfileSection userSelectionData={userSelectionData} lang={lang as "en" | "tr"} />
|
||||
) : (
|
||||
<div className="text-center text-gray-500">{t.loading}</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
@@ -60,32 +70,14 @@ const ClientMenu: React.FC<ClientMenuProps> = ({ lang = "en", activePage }) => {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Profile Section with Suspense */}
|
||||
<div className="p-4 border-b border-gray-200 bg-gray-50">
|
||||
<Suspense
|
||||
fallback={<div className="text-center py-4">{t.loading}</div>}
|
||||
>
|
||||
{loading ? (
|
||||
<ProfileLoadingState loadingText={t.loading} />
|
||||
) : userType === "employee" && userSelectionData ? (
|
||||
<EmployeeProfileSection
|
||||
userSelectionData={userSelectionData}
|
||||
lang={lang as "en" | "tr"}
|
||||
/>
|
||||
) : userType === "occupant" && userSelectionData ? (
|
||||
<OccupantProfileSection
|
||||
userSelectionData={userSelectionData}
|
||||
lang={lang as "en" | "tr"}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center text-gray-500">{t.loading}</div>
|
||||
)}
|
||||
{createProfileComponent()}
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu
|
||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} /> */}
|
||||
<NavigationMenu activePage={activePage} lang={lang} />
|
||||
<NavigationMenu activePage={activePage} lang={lang as "en" | "tr"} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
21
WebServices/management-frontend/src/components/menu/type.ts
Normal file
21
WebServices/management-frontend/src/components/menu/type.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const NavigationLanguage = {
|
||||
en: {
|
||||
"/dashboard": "Dashboard",
|
||||
"/append/event": "Event Board",
|
||||
"/append/service": "Service Board",
|
||||
"/application": "Application Board",
|
||||
},
|
||||
tr: {
|
||||
"/dashboard": "Kontrol Paneli",
|
||||
"/append/event": "Event Paneli",
|
||||
"/append/service": "Servis Paneli",
|
||||
"/application": "Uygulama Paneli",
|
||||
},
|
||||
};
|
||||
|
||||
export function getNavigationMenu(lang: string) {
|
||||
return (
|
||||
NavigationLanguage[lang as keyof typeof NavigationLanguage] ||
|
||||
NavigationLanguage.en
|
||||
);
|
||||
}
|
||||
@@ -12,17 +12,15 @@ import { CardDisplay } from "@/components/common/CardDisplay/CardDisplay";
|
||||
import { FormMode } from "@/components/common/FormDisplay/types";
|
||||
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
||||
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||
import { LanguageSelectionComponent } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||
import { getCreateApplicationSchema, getUpdateApplicationSchema } from "./schema";
|
||||
import { translations } from "./language";
|
||||
import { PageProps } from "@/validations/translations/translation";
|
||||
import { useApiData } from "@/components/common";
|
||||
import { Language } from "@/components/common/schemas";
|
||||
|
||||
const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
// Add local state for language to ensure it persists when changed
|
||||
const [lang, setLang] = useState<Language>(initialLang as Language);
|
||||
const ApplicationPage: React.FC<PageProps> = ({ lang }: { lang: Language }) => {
|
||||
|
||||
// Use the API data hook directly
|
||||
const {
|
||||
data,
|
||||
pagination,
|
||||
@@ -45,8 +43,8 @@ const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
);
|
||||
|
||||
const [validationSchema, setValidationSchema] = useState(() =>
|
||||
mode === 'create' ? getCreateApplicationSchema(lang as "en" | "tr") :
|
||||
mode === 'update' ? getUpdateApplicationSchema(lang as "en" | "tr") :
|
||||
mode === 'create' ? getCreateApplicationSchema(lang) :
|
||||
mode === 'update' ? getUpdateApplicationSchema(lang) :
|
||||
schema.ViewApplicationSchema
|
||||
);
|
||||
|
||||
@@ -63,8 +61,8 @@ const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
// Update validation schema when mode or language changes
|
||||
useEffect(() => {
|
||||
setValidationSchema(
|
||||
mode === 'create' ? getCreateApplicationSchema(lang as "en" | "tr") :
|
||||
mode === 'update' ? getUpdateApplicationSchema(lang as "en" | "tr") :
|
||||
mode === 'create' ? getCreateApplicationSchema(lang) :
|
||||
mode === 'update' ? getUpdateApplicationSchema(lang) :
|
||||
schema.ViewApplicationSchema
|
||||
);
|
||||
}, [mode, lang]);
|
||||
@@ -98,7 +96,7 @@ const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
additionalFields: [
|
||||
// {
|
||||
// name: "status",
|
||||
// label: translations[lang as "en" | "tr"].status,
|
||||
// label: translations[lang].status,
|
||||
// type: "select" as const,
|
||||
// options: [
|
||||
// { value: "active", label: "Active" },
|
||||
@@ -170,13 +168,6 @@ const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
/>
|
||||
|
||||
{/* Language Selection */}
|
||||
<LanguageSelectionComponent
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
setLang={setLang}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ import { PageProps } from "../validations/translations/translation";
|
||||
import { UnAuthorizedPage } from "./unauthorizedpage";
|
||||
import menuPages from "./index";
|
||||
|
||||
export function retrievePageByUrl(url: string): React.FC<PageProps> {
|
||||
export function retrievePageByUrl(url: string, lang: "en" | "tr"): React.FC<PageProps> {
|
||||
if (url in menuPages) {
|
||||
return menuPages[url as keyof typeof menuPages];
|
||||
const PageComponent = menuPages[url as keyof typeof menuPages];
|
||||
// Return a new component that passes the lang prop to the original component
|
||||
return (props: PageProps) => <PageComponent {...props} lang={lang} />;
|
||||
}
|
||||
return UnAuthorizedPage;
|
||||
// Also pass lang to UnAuthorizedPage
|
||||
return (props: PageProps) => <UnAuthorizedPage {...props} lang={lang} />;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ interface FilteredMenuFirstLevel {
|
||||
|
||||
interface PageProps {
|
||||
lang: keyof LanguageTranslation;
|
||||
queryParams: { [key: string]: string | undefined };
|
||||
queryParams?: { [key: string]: string | undefined };
|
||||
}
|
||||
|
||||
type PageComponent = React.ComponentType<PageProps>;
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../menu/EmployeeProfileSection.tsx",
|
||||
"src/components/menu/NavigationMenu.tsx",
|
||||
"../../menu/menu.tsx"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { PageComponent } from "@/components/validations/translations/translation";
|
||||
import PeopleSuperUserApp from "@/components/Pages/people/superusers/app";
|
||||
|
||||
export const PageNavigator: Record<string, Record<string, PageComponent>> = {
|
||||
"/individual": {
|
||||
app000003: PeopleSuperUserApp,
|
||||
},
|
||||
};
|
||||
@@ -1,215 +0,0 @@
|
||||
// Mock data for buildings table
|
||||
const buildingsMockData = [
|
||||
{
|
||||
uu_id: "63192f8a-0b36-49b5-a058-423eb375ab1b",
|
||||
gov_address_code: "GAC12345678",
|
||||
build_name: "Sunset Towers",
|
||||
build_no: "A123",
|
||||
max_floor: 15,
|
||||
underground_floor: 2,
|
||||
build_date: "2010-05-12T00:00:00Z",
|
||||
decision_period_date: "2024-03-15T00:00:00Z",
|
||||
tax_no: "TX123456789012",
|
||||
lift_count: 3,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 2,
|
||||
security_service_man_count: 1,
|
||||
garage_count: 25,
|
||||
site_uu_id: "site-uuid-6789abcd-1234",
|
||||
address_uu_id: "addr-uuid-1234-5678-abcd",
|
||||
build_types_uu_id: "type-uuid-residential-apt",
|
||||
},
|
||||
{
|
||||
uu_id: "8149fcac-3ac8-4107-acce-ef52f378a874",
|
||||
gov_address_code: "GAC23456789",
|
||||
build_name: "Ocean View Plaza",
|
||||
build_no: "B456",
|
||||
max_floor: 22,
|
||||
underground_floor: 3,
|
||||
build_date: "2015-07-23T00:00:00Z",
|
||||
decision_period_date: "2024-05-10T00:00:00Z",
|
||||
tax_no: "TX234567890123",
|
||||
lift_count: 5,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 3,
|
||||
security_service_man_count: 2,
|
||||
garage_count: 50,
|
||||
site_uu_id: "site-uuid-7890bcde-2345",
|
||||
address_uu_id: "addr-uuid-2345-6789-bcde",
|
||||
build_types_uu_id: "type-uuid-residential-condo",
|
||||
},
|
||||
{
|
||||
uu_id: "10fb6ffe-610b-4e7e-bb5b-b46e0946cff7",
|
||||
gov_address_code: "GAC34567890",
|
||||
build_name: "Parkside Heights",
|
||||
build_no: "C789",
|
||||
max_floor: 8,
|
||||
underground_floor: 1,
|
||||
build_date: "2005-11-30T00:00:00Z",
|
||||
decision_period_date: "2024-04-22T00:00:00Z",
|
||||
tax_no: "TX345678901234",
|
||||
lift_count: 2,
|
||||
heating_system: true,
|
||||
cooling_system: false,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 1,
|
||||
security_service_man_count: 1,
|
||||
garage_count: 16,
|
||||
site_uu_id: "site-uuid-8901cdef-3456",
|
||||
address_uu_id: "addr-uuid-3456-7890-cdef",
|
||||
build_types_uu_id: "type-uuid-commercial-office",
|
||||
},
|
||||
{
|
||||
uu_id: "0447123a-8992-4e22-ba86-2f0feaa763d2",
|
||||
gov_address_code: "GAC45678901",
|
||||
build_name: "Riverside Apartments",
|
||||
build_no: "D012",
|
||||
max_floor: 12,
|
||||
underground_floor: 2,
|
||||
build_date: "2018-03-17T00:00:00Z",
|
||||
decision_period_date: "2024-02-28T00:00:00Z",
|
||||
tax_no: "TX456789012345",
|
||||
lift_count: 3,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: false,
|
||||
block_service_man_count: 2,
|
||||
security_service_man_count: 1,
|
||||
garage_count: 30,
|
||||
site_uu_id: "site-uuid-9012defg-4567",
|
||||
address_uu_id: "addr-uuid-4567-8901-defg",
|
||||
build_types_uu_id: "type-uuid-residential-apt",
|
||||
},
|
||||
{
|
||||
uu_id: "6682a927-abb7-4d33-b877-3df170c3679c",
|
||||
gov_address_code: "GAC56789012",
|
||||
build_name: "Mountain View Plaza",
|
||||
build_no: "E345",
|
||||
max_floor: 5,
|
||||
underground_floor: 0,
|
||||
build_date: "2000-09-05T00:00:00Z",
|
||||
decision_period_date: "2024-01-15T00:00:00Z",
|
||||
tax_no: "TX567890123456",
|
||||
lift_count: 1,
|
||||
heating_system: true,
|
||||
cooling_system: false,
|
||||
hot_water_system: false,
|
||||
block_service_man_count: 1,
|
||||
security_service_man_count: 0,
|
||||
garage_count: 8,
|
||||
site_uu_id: "site-uuid-0123efgh-5678",
|
||||
address_uu_id: "addr-uuid-5678-9012-efgh",
|
||||
build_types_uu_id: "type-uuid-mixed-use",
|
||||
},
|
||||
{
|
||||
uu_id: "a06fef1b-3eb7-4aed-b901-47a171a12a93",
|
||||
gov_address_code: "GAC67890123",
|
||||
build_name: "City Center Tower",
|
||||
build_no: "F678",
|
||||
max_floor: 30,
|
||||
underground_floor: 4,
|
||||
build_date: "2020-01-10T00:00:00Z",
|
||||
decision_period_date: "2024-06-30T00:00:00Z",
|
||||
tax_no: "TX678901234567",
|
||||
lift_count: 8,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 4,
|
||||
security_service_man_count: 3,
|
||||
garage_count: 100,
|
||||
site_uu_id: "site-uuid-1234fghi-6789",
|
||||
address_uu_id: "addr-uuid-6789-0123-fghi",
|
||||
build_types_uu_id: "type-uuid-commercial-skyscraper",
|
||||
},
|
||||
{
|
||||
uu_id: "22be0407-f6a4-456e-a183-6641d2714d73",
|
||||
gov_address_code: "GAC78901234",
|
||||
build_name: "Garden Villas",
|
||||
build_no: "G901",
|
||||
max_floor: 3,
|
||||
underground_floor: 0,
|
||||
build_date: "2012-06-22T00:00:00Z",
|
||||
decision_period_date: "2024-03-01T00:00:00Z",
|
||||
tax_no: "TX789012345678",
|
||||
lift_count: 0,
|
||||
heating_system: true,
|
||||
cooling_system: false,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 1,
|
||||
security_service_man_count: 0,
|
||||
garage_count: 6,
|
||||
site_uu_id: "site-uuid-2345ghij-7890",
|
||||
address_uu_id: "addr-uuid-7890-1234-ghij",
|
||||
build_types_uu_id: "type-uuid-residential-townhouse",
|
||||
},
|
||||
{
|
||||
uu_id: "7792645f-350c-4567-8a78-190014674e6b",
|
||||
gov_address_code: "GAC89012345",
|
||||
build_name: "Industrial Complex",
|
||||
build_no: "H234",
|
||||
max_floor: 2,
|
||||
underground_floor: 1,
|
||||
build_date: "2008-12-05T00:00:00Z",
|
||||
decision_period_date: "2024-05-15T00:00:00Z",
|
||||
tax_no: "TX890123456789",
|
||||
lift_count: 1,
|
||||
heating_system: true,
|
||||
cooling_system: false,
|
||||
hot_water_system: false,
|
||||
block_service_man_count: 0,
|
||||
security_service_man_count: 1,
|
||||
garage_count: 12,
|
||||
site_uu_id: "site-uuid-3456hijk-8901",
|
||||
address_uu_id: "addr-uuid-8901-2345-hijk",
|
||||
build_types_uu_id: "type-uuid-industrial",
|
||||
},
|
||||
{
|
||||
uu_id: "8de7a620-3c1e-4925-8147-3eb33a2059cc",
|
||||
gov_address_code: "GAC90123456",
|
||||
build_name: "Hillside Residences",
|
||||
build_no: "I567",
|
||||
max_floor: 10,
|
||||
underground_floor: 2,
|
||||
build_date: "2017-04-18T00:00:00Z",
|
||||
decision_period_date: "2024-02-10T00:00:00Z",
|
||||
tax_no: "TX901234567890",
|
||||
lift_count: 2,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 2,
|
||||
security_service_man_count: 1,
|
||||
garage_count: 20,
|
||||
site_uu_id: "site-uuid-4567ijkl-9012",
|
||||
address_uu_id: "addr-uuid-9012-3456-ijkl",
|
||||
build_types_uu_id: "type-uuid-residential-apt",
|
||||
},
|
||||
{
|
||||
uu_id: "1a680003-d005-414c-86ab-f16e090aba25",
|
||||
gov_address_code: "GACA0123456",
|
||||
build_name: "Tech Hub Center",
|
||||
build_no: "J890",
|
||||
max_floor: 18,
|
||||
underground_floor: 3,
|
||||
build_date: "2019-08-30T00:00:00Z",
|
||||
decision_period_date: "2024-04-01T00:00:00Z",
|
||||
tax_no: "TXA01234567890",
|
||||
lift_count: 6,
|
||||
heating_system: true,
|
||||
cooling_system: true,
|
||||
hot_water_system: true,
|
||||
block_service_man_count: 3,
|
||||
security_service_man_count: 2,
|
||||
garage_count: 45,
|
||||
site_uu_id: "site-uuid-5678jklm-0123",
|
||||
address_uu_id: "addr-uuid-0123-4567-jklm",
|
||||
build_types_uu_id: "type-uuid-commercial-office",
|
||||
},
|
||||
];
|
||||
|
||||
export default buildingsMockData;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import { PageProps } from "./interFaces";
|
||||
|
||||
const pageContext = {
|
||||
tr: {
|
||||
pageTitle: "Sayfa 0001",
|
||||
pageDescription: "Bu, Sayfa 0001'in içeriğidir.",
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Page 0001",
|
||||
pageDescription: "This is the content of Page 0001.",
|
||||
},
|
||||
};
|
||||
|
||||
function Page0001({ lang }: PageProps) {
|
||||
const { pageTitle, pageDescription } =
|
||||
pageContext[lang as keyof typeof pageContext];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-screen">
|
||||
<header className="bg-gray-800 text-white p-4 text-center">
|
||||
<h1 className="text-2xl font-bold">{pageTitle}</h1>
|
||||
</header>
|
||||
<main className="flex-grow p-4 bg-gray-100">
|
||||
<p className="text-gray-700">{pageDescription}</p>
|
||||
</main>
|
||||
<footer className="bg-gray-800 text-white p-4 text-center">
|
||||
<p>© 2023 My Application</p>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page0001;
|
||||
@@ -1,113 +0,0 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Home } from "lucide-react";
|
||||
import { transformMenu, LanguageTranslation } from "@/components/menu/runner";
|
||||
|
||||
async function LeftMenu({
|
||||
searchParams,
|
||||
pageUuidList,
|
||||
lang,
|
||||
pageSelected,
|
||||
}: {
|
||||
pageUuidList: string[];
|
||||
lang: keyof LanguageTranslation;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
pageSelected: string;
|
||||
}) {
|
||||
const transformedMenu = transformMenu(pageUuidList) || [];
|
||||
|
||||
// Get the menuContext from searchParams without setting a default value
|
||||
const menuContext = searchParams?.menu;
|
||||
|
||||
// Only parse the indices if menuContext exists
|
||||
let firstLayerIndex = -1;
|
||||
let secondLayerIndex = -1;
|
||||
|
||||
if (menuContext) {
|
||||
const indices = menuContext.toString().split("*").map(Number);
|
||||
firstLayerIndex = indices[0] || 0;
|
||||
secondLayerIndex = indices[1] || 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="flex flex-col space-y-2">
|
||||
<div className="text-xl font-bold mb-6 text-center">Dashboard</div>
|
||||
{transformedMenu &&
|
||||
transformedMenu.map((item, firstIndex) => (
|
||||
<div key={item.name} className="mb-4">
|
||||
<Link
|
||||
href={`${pageSelected}?menu=${firstIndex}*0`}
|
||||
className={`text-xl font-semibold pl-5 my-2 py-2 block ${
|
||||
firstIndex === firstLayerIndex
|
||||
? "text-emerald-600"
|
||||
: "text-emerald-400"
|
||||
} hover:text-emerald-600`}
|
||||
>
|
||||
{item.lg[lang]}
|
||||
</Link>
|
||||
|
||||
{/* Only render the second layer if menuContext exists and this first layer item is selected */}
|
||||
{menuContext && firstIndex === firstLayerIndex && (
|
||||
<ul className="space-y-2">
|
||||
{item.subList.map((subItem, secondIndex) => (
|
||||
<div key={subItem.name}>
|
||||
<Link
|
||||
href={`${pageSelected}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`ml-5 my-4 pl-4 text-xl font-semibold block ${
|
||||
secondIndex === secondLayerIndex
|
||||
? "text-emerald-700"
|
||||
: "text-emerald-500"
|
||||
} hover:text-emerald-700`}
|
||||
>
|
||||
{subItem.lg[lang]}
|
||||
</Link>
|
||||
{/* Only render the third layer if this second layer item is selected */}
|
||||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="ml-5">
|
||||
{subItem.subList.map((subSubItem) =>
|
||||
`${pageSelected}` !== subSubItem.siteUrl ? (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={`${subSubItem?.siteUrl}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`flex flex-row text-xl py-4 my-4 w-full space-x-2 p-2 rounded hover:bg-gray-200`}
|
||||
>
|
||||
<span className="text-gray-400">
|
||||
<Home />
|
||||
</span>
|
||||
<span className="ml-5 text-gray-700">
|
||||
{subSubItem.lg[lang]}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
key={subSubItem.name}
|
||||
href={`${subSubItem?.siteUrl}?menu=${firstIndex}*${secondIndex}`}
|
||||
className={`flex flex-row text-xl py-4 my-4 w-full space-x-2 p-2 rounded bg-gray-100 cursor-not-allowed"`}
|
||||
>
|
||||
<span className="text-gray-400">
|
||||
<Home />
|
||||
</span>
|
||||
<span className="ml-5 text-gray-700">
|
||||
{subSubItem.lg[lang]}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LeftMenu;
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Filters the menu structure based on intersections with provided UUIDs
|
||||
* @param {string[]} uuids - Array of UUIDs to check for intersection
|
||||
* @param {Array} menu - The original menu structure
|
||||
* @returns {Array} - Filtered menu structure with only matching items
|
||||
*/
|
||||
import Menu from "@/components/menu/store"; // Assuming you have a menu structure imported
|
||||
import {
|
||||
MenuFirstLevel,
|
||||
MenuSecondLevel,
|
||||
MenuThirdLevel,
|
||||
FilteredMenuFirstLevel,
|
||||
FilteredMenuSecondLevel,
|
||||
FilteredMenuThirdLevel,
|
||||
} from "@/components/validations/translations/tr";
|
||||
|
||||
|
||||
function transformMenu(siteUrls: string[]) {
|
||||
// Process the menu structure
|
||||
const filteredMenu: FilteredMenuFirstLevel[] = Menu.reduce(
|
||||
(acc: FilteredMenuFirstLevel[], firstLevel: MenuFirstLevel) => {
|
||||
// Create a new first level item with empty subList
|
||||
const newFirstLevel: FilteredMenuFirstLevel = {
|
||||
name: firstLevel.name,
|
||||
lg: { ...firstLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process second level items
|
||||
firstLevel.subList.forEach((secondLevel: MenuSecondLevel) => {
|
||||
// Create a new second level item with empty subList
|
||||
const newSecondLevel: FilteredMenuSecondLevel = {
|
||||
name: secondLevel.name,
|
||||
lg: { ...secondLevel.lg },
|
||||
subList: [],
|
||||
};
|
||||
|
||||
// Process third level items
|
||||
secondLevel.subList.forEach((thirdLevel: MenuThirdLevel) => {
|
||||
// Check if the third level's siteUrl matches exactly
|
||||
if (
|
||||
thirdLevel.siteUrl &&
|
||||
siteUrls.some((url) => url === thirdLevel.siteUrl)
|
||||
) {
|
||||
// Create a modified third level item
|
||||
const newThirdLevel: FilteredMenuThirdLevel = {
|
||||
name: thirdLevel.name,
|
||||
lg: { ...thirdLevel.lg },
|
||||
siteUrl: thirdLevel.siteUrl,
|
||||
};
|
||||
|
||||
// Add the modified third level to the second level's subList
|
||||
newSecondLevel.subList.push(newThirdLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the second level to the first level if it has any matching third level items
|
||||
if (newSecondLevel.subList.length > 0) {
|
||||
newFirstLevel.subList.push(newSecondLevel);
|
||||
}
|
||||
});
|
||||
|
||||
// Only add the first level to the result if it has any matching second level items
|
||||
if (newFirstLevel.subList.length > 0) {
|
||||
acc.push(newFirstLevel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return filteredMenu;
|
||||
}
|
||||
|
||||
export { transformMenu };
|
||||
@@ -1,203 +0,0 @@
|
||||
const Individual = {
|
||||
name: "Individual",
|
||||
lg: {
|
||||
tr: "Birey",
|
||||
en: "Individual",
|
||||
},
|
||||
siteUrl: "/individual",
|
||||
};
|
||||
|
||||
const User = {
|
||||
name: "User",
|
||||
lg: {
|
||||
tr: "Kullanıcı",
|
||||
en: "User",
|
||||
},
|
||||
siteUrl: "/user",
|
||||
};
|
||||
|
||||
const Build = {
|
||||
name: "Build",
|
||||
lg: {
|
||||
tr: "Apartman",
|
||||
en: "Build",
|
||||
},
|
||||
siteUrl: "/build",
|
||||
};
|
||||
|
||||
const Dashboard = {
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
siteUrl: "/dashboard",
|
||||
};
|
||||
|
||||
const BuildParts = {
|
||||
name: "BuildParts",
|
||||
lg: {
|
||||
tr: "Daireler",
|
||||
en: "Build Parts",
|
||||
},
|
||||
siteUrl: "/build/parts",
|
||||
};
|
||||
|
||||
const BuildArea = {
|
||||
name: "BuildArea",
|
||||
lg: {
|
||||
tr: "Daire Alanları",
|
||||
en: "Build Area",
|
||||
},
|
||||
siteUrl: "/build/area",
|
||||
};
|
||||
|
||||
const ManagementAccounting = {
|
||||
name: "ManagementAccounting",
|
||||
lg: {
|
||||
tr: "Yönetim Cari Hareketler",
|
||||
en: "ManagementAccounting",
|
||||
},
|
||||
siteUrl: "/management/accounting",
|
||||
};
|
||||
|
||||
const ManagementBudget = {
|
||||
name: "ManagementBudget",
|
||||
lg: {
|
||||
tr: "Yönetim Bütçe İşlemleri",
|
||||
en: "Management Budget",
|
||||
},
|
||||
siteUrl: "/management/budget",
|
||||
};
|
||||
|
||||
const BuildPartsAccounting = {
|
||||
name: "BuildPartsAccounting",
|
||||
lg: {
|
||||
tr: "Daire Cari Hareketler",
|
||||
en: "Build Parts Accounting",
|
||||
},
|
||||
siteUrl: "/build/parts/accounting",
|
||||
};
|
||||
|
||||
const AnnualMeeting = {
|
||||
name: "AnnualMeeting",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı Tanımlama ve Davet",
|
||||
en: "Annual Meetings and Invitations",
|
||||
},
|
||||
siteUrl: "/annual/meeting",
|
||||
};
|
||||
|
||||
const AnnualMeetingClose = {
|
||||
name: "AnnualMeetingClose",
|
||||
lg: {
|
||||
tr: "Yıllık Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Annual Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/annual/meeting/close",
|
||||
};
|
||||
|
||||
const EmergencyMeeting = {
|
||||
name: "EmergencyMeeting",
|
||||
lg: {
|
||||
tr: "Acil Toplantı Tanımlama ve Davet",
|
||||
en: "Emergency Meeting and Invitations",
|
||||
},
|
||||
siteUrl: "/emergency/meeting",
|
||||
};
|
||||
|
||||
const EmergencyMeetingClose = {
|
||||
name: "EmergencyMeetingClose",
|
||||
lg: {
|
||||
tr: "Acil Olağan Toplantı kapatma ve Cari Yaratma",
|
||||
en: "Emergency Meeting Close and Accountings",
|
||||
},
|
||||
siteUrl: "/emergency/meeting/close",
|
||||
};
|
||||
|
||||
const MeetingParticipations = {
|
||||
name: "MeetingParticipations",
|
||||
lg: {
|
||||
tr: "Toplantı Katılım İşlemleri",
|
||||
en: "Meeting Participations",
|
||||
},
|
||||
siteUrl: "/meeting/participation",
|
||||
};
|
||||
|
||||
const Menu = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
lg: {
|
||||
tr: "Pano",
|
||||
en: "Dashboard",
|
||||
},
|
||||
subList: [Dashboard],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Definitions",
|
||||
lg: {
|
||||
tr: "Tanımlar",
|
||||
en: "Definitions",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "People",
|
||||
lg: {
|
||||
tr: "Kişiler",
|
||||
en: "People",
|
||||
},
|
||||
subList: [Individual, User],
|
||||
},
|
||||
{
|
||||
name: "Building",
|
||||
lg: {
|
||||
tr: "Binalar",
|
||||
en: "Building",
|
||||
},
|
||||
subList: [Build, BuildParts, BuildArea],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Building Management",
|
||||
lg: {
|
||||
tr: "Bina Yönetimi",
|
||||
en: "Building Management",
|
||||
},
|
||||
subList: [
|
||||
{
|
||||
name: "Management Accounting",
|
||||
lg: {
|
||||
tr: "Cari işlemler",
|
||||
en: "Management Accounting",
|
||||
},
|
||||
subList: [ManagementAccounting, ManagementBudget, BuildPartsAccounting],
|
||||
},
|
||||
{
|
||||
name: "Meetings",
|
||||
lg: {
|
||||
tr: "Toplantılar",
|
||||
en: "Meetings",
|
||||
},
|
||||
subList: [
|
||||
AnnualMeeting,
|
||||
AnnualMeetingClose,
|
||||
EmergencyMeeting,
|
||||
EmergencyMeetingClose,
|
||||
MeetingParticipations,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default Menu;
|
||||
Reference in New Issue
Block a user