updated lang change and FormDisplay Components

This commit is contained in:
Berkay 2025-04-30 14:30:22 +03:00
parent f2cc7a69b5
commit 36e63960f8
87 changed files with 5517 additions and 312 deletions

View File

@ -34,6 +34,7 @@ npx shadcn@latest add calendar -y
npx shadcn@latest add date-picker -y npx shadcn@latest add date-picker -y
npx shadcn@latest add skeleton -y npx shadcn@latest add skeleton -y
npx shadcn@latest add table -y npx shadcn@latest add table -y
npx shadcn@latest add textarea -y
# Update any dependencies with legacy peer deps # Update any dependencies with legacy peer deps
echo "🔄 Updating dependencies..." echo "🔄 Updating dependencies..."

View File

@ -1,54 +1,28 @@
"use server"; "use server";
import React from "react"; import React from "react";
import ClientMenu from "@/components/menu/menu"; import DashboardLayout from "@/components/layouts/DashboardLayout";
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token"; import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
import { searchPlaceholder } from "@/app/commons/pageDefaults";
const pageInfo = {
tr: "Birey Sayfası",
en: "Individual Page",
};
export default async function Dashboard({ export default async function Dashboard({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ [key: string]: string | undefined }>; searchParams: Promise<{ [key: string]: string | undefined }>;
}) { }) {
const activePage = "/individual"; // Use the enhanced dashboard hook to get all necessary data
const siteUrlsList = (await retrievePageList()) || []; const {
const pageToDirect = await retrievePagebyUrl(activePage); activePage,
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage); searchParamsInstance,
const searchParamsInstance = await searchParams; lang,
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en"; PageComponent,
siteUrlsList
} = await useDashboardPage({
pageUrl: "/individual",
searchParams
});
return ( return (
<> <DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList}>
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden"> <PageComponent lang={lang} queryParams={searchParamsInstance} />
{/* Sidebar */} </DashboardLayout>
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<ClientMenu siteUrls={siteUrlsList} lang={lang} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4">
{/* Sticky Header */}
<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>
<div className="p-4 overflow-y-auto">
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>
</div>
</>
); );
} }

View File

@ -18,7 +18,7 @@ export default async function DashLayout({
} }
return ( return (
<div className="h-screen w-full"> <div className="h-screen w-full">
<div className="h-full w-full overflow-y-auto">{children}</div> <div className="h-full w-full">{children}</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,25 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
interface CreateButtonProps {
onClick: () => void;
translations: Record<string, any>;
lang: string;
}
export const CreateButton: React.FC<CreateButtonProps> = ({
onClick,
translations,
lang,
}) => {
const t = translations[lang] || {};
return (
<Button onClick={onClick} className="flex items-center">
<Plus className="mr-2 h-4 w-4" />
{t.create || "Create"}
</Button>
);
};

View File

@ -0,0 +1,31 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { CustomButton } from "./types";
import { cn } from "@/lib/utils";
interface CustomButtonComponentProps {
button: CustomButton;
isSelected: boolean;
onClick: () => void;
}
export const CustomButtonComponent: React.FC<CustomButtonComponentProps> = ({
button,
isSelected,
onClick,
}) => {
return (
<Button
onClick={onClick}
variant={button.variant || "default"}
className={cn(
"flex items-center",
isSelected && "ring-2 ring-primary ring-offset-2"
)}
>
{button.icon && <span className="mr-2">{button.icon}</span>}
{button.label}
</Button>
);
};

View File

@ -0,0 +1,3 @@
export * from './CreateButton';
export * from './CustomButtonComponent';
export * from './types';

View File

@ -0,0 +1,17 @@
import { ReactNode } from "react";
export interface CustomButton {
id: string;
label: string;
onClick: () => void;
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
icon?: ReactNode;
}
export interface ActionButtonsProps {
onCreateClick: () => void;
translations: Record<string, any>;
lang: string;
customButtons?: CustomButton[];
defaultSelectedButtonId?: string;
}

View File

@ -0,0 +1,73 @@
"use client";
import React from "react";
import { CardItem } from "./CardItem";
import { CardSkeleton } from "./CardSkeleton";
import { getFieldValue, getGridClasses } from "./utils";
import { CardDisplayProps } from "./schema";
// Interface moved to schema.ts
export function CardDisplay<T>({
showFields,
data,
lang,
translations,
error,
loading,
titleField = "name",
onCardClick,
renderCustomField,
gridCols = 4,
showViewIcon = false,
showUpdateIcon = false,
onViewClick,
onUpdateClick,
}: CardDisplayProps<T>) {
if (error) {
return (
<div className="p-6 text-center text-red-500">
{error.message || "An error occurred while fetching data."}
</div>
);
}
return (
<div className={getGridClasses(gridCols)}>
{loading ? (
// Loading skeletons
Array.from({ length: 10 }).map((_, index) => (
<CardSkeleton
key={`loading-${index}`}
index={index}
showFields={showFields}
showViewIcon={showViewIcon}
showUpdateIcon={showUpdateIcon}
/>
))
) : data.length === 0 ? (
<div className="col-span-full text-center py-6">
{(translations[lang] || {}).noData || "No data found"}
</div>
) : (
data.map((item, index) => (
<CardItem
key={index}
item={item}
index={index}
showFields={showFields}
titleField={titleField}
lang={lang}
translations={translations}
onCardClick={onCardClick}
renderCustomField={renderCustomField}
showViewIcon={showViewIcon}
showUpdateIcon={showUpdateIcon}
onViewClick={onViewClick}
onUpdateClick={onUpdateClick}
getFieldValue={getFieldValue}
/>
))
)}
</div>
);
}

View File

@ -0,0 +1,130 @@
"use client";
import React from "react";
import {
Card,
CardContent,
CardHeader,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Eye, Edit } from "lucide-react";
import { CardItemProps, CardActionsProps, CardFieldProps } from "./schema";
export function CardItem<T>({
item,
index,
showFields,
titleField,
lang,
translations,
onCardClick,
renderCustomField,
showViewIcon,
showUpdateIcon,
onViewClick,
onUpdateClick,
getFieldValue,
}: CardItemProps<T>) {
return (
<div key={index} className="w-full p-1">
<Card
className={`h-full ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}`}
onClick={onCardClick ? () => onCardClick(item) : undefined}
>
<CardHeader className="p-3 pb-0 flex justify-between items-start">
<h3 className="text-lg font-semibold">
{getFieldValue(item, titleField)}
</h3>
<CardActions
item={item}
showViewIcon={showViewIcon}
showUpdateIcon={showUpdateIcon}
onViewClick={onViewClick}
onUpdateClick={onUpdateClick}
/>
</CardHeader>
<CardContent className="p-3">
<div className="space-y-2">
{showFields.map((field) => (
<CardField
key={`${index}-${field}`}
item={item}
field={field}
lang={lang}
translations={translations}
renderCustomField={renderCustomField}
getFieldValue={getFieldValue}
/>
))}
</div>
</CardContent>
</Card>
</div>
);
}
// Interface moved to schema.ts
function CardActions<T>({
item,
showViewIcon,
showUpdateIcon,
onViewClick,
onUpdateClick,
}: CardActionsProps<T>) {
if (!showViewIcon && !showUpdateIcon) return null;
return (
<div className="flex space-x-1">
{showViewIcon && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
if (onViewClick) onViewClick(item);
}}
>
<Eye className="h-4 w-4" />
</Button>
)}
{showUpdateIcon && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
if (onUpdateClick) onUpdateClick(item);
}}
>
<Edit className="h-4 w-4" />
</Button>
)}
</div>
);
}
// Interface moved to schema.ts
function CardField<T>({
item,
field,
lang,
translations,
renderCustomField,
getFieldValue,
}: CardFieldProps<T>) {
return (
<div className="flex">
<span className="font-medium mr-2 min-w-[80px]">
{translations[field]?.[lang] || field}:
</span>
<span className="flex-1">
{renderCustomField
? renderCustomField(item, field)
: getFieldValue(item, field)}
</span>
</div>
);
}

View File

@ -0,0 +1,46 @@
"use client";
import React from "react";
import {
Card,
CardContent,
CardHeader,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { CardSkeletonProps } from "./schema";
// Interface moved to schema.ts
export function CardSkeleton({
index,
showFields,
showViewIcon,
showUpdateIcon,
}: CardSkeletonProps) {
return (
<div key={`loading-${index}`} className="w-full p-1">
<Card className="h-full">
<CardHeader className="p-3 pb-0 flex justify-between items-start">
<Skeleton className="h-5 w-3/4" />
<div className="flex space-x-1">
{showViewIcon && (
<Skeleton className="h-8 w-8 rounded-full" />
)}
{showUpdateIcon && (
<Skeleton className="h-8 w-8 rounded-full" />
)}
</div>
</CardHeader>
<CardContent className="p-3">
<div className="space-y-2">
{showFields.map((field, fieldIndex) => (
<div key={`loading-${index}-${field}`} className="flex">
<Skeleton className="h-4 w-10 mr-2" />
<Skeleton className="h-4 w-full" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1 @@
export { CardDisplay } from './CardDisplay';

View File

@ -0,0 +1,117 @@
/**
* CardDisplay component interfaces
*/
/**
* Main props for the CardDisplay component
*/
export interface CardDisplayProps<T> {
/** Fields to display in each card */
showFields: string[];
/** Array of data items to display */
data: T[];
/** Current language code */
lang: string;
/** Translations object for field labels and messages */
translations: Record<string, any>;
/** Error object if data fetching failed */
error: Error | null;
/** Loading state indicator */
loading: boolean;
/** Field to use as the card title (default: "name") */
titleField?: string;
/** Handler for when a card is clicked */
onCardClick?: (item: T) => void;
/** Custom renderer for specific fields */
renderCustomField?: (item: T, field: string) => React.ReactNode;
/** Number of columns in the grid (1-6) */
gridCols?: 1 | 2 | 3 | 4 | 5 | 6;
/** Whether to show the view icon */
showViewIcon?: boolean;
/** Whether to show the update/edit icon */
showUpdateIcon?: boolean;
/** Handler for when the view icon is clicked */
onViewClick?: (item: T) => void;
/** Handler for when the update/edit icon is clicked */
onUpdateClick?: (item: T) => void;
}
/**
* Props for the CardItem component
*/
export interface CardItemProps<T> {
/** Data item to display */
item: T;
/** Index of the item in the data array */
index: number;
/** Fields to display in the card */
showFields: string[];
/** Field to use as the card title */
titleField: string;
/** Current language code */
lang: string;
/** Translations object for field labels */
translations: Record<string, any>;
/** Handler for when the card is clicked */
onCardClick?: (item: T) => void;
/** Custom renderer for specific fields */
renderCustomField?: (item: T, field: string) => React.ReactNode;
/** Whether to show the view icon */
showViewIcon: boolean;
/** Whether to show the update/edit icon */
showUpdateIcon: boolean;
/** Handler for when the view icon is clicked */
onViewClick?: (item: T) => void;
/** Handler for when the update/edit icon is clicked */
onUpdateClick?: (item: T) => void;
/** Function to get field values from the item */
getFieldValue: (item: any, field: string) => any;
}
/**
* Props for the CardActions component
*/
export interface CardActionsProps<T> {
/** Data item the actions apply to */
item: T;
/** Whether to show the view icon */
showViewIcon: boolean;
/** Whether to show the update/edit icon */
showUpdateIcon: boolean;
/** Handler for when the view icon is clicked */
onViewClick?: (item: T) => void;
/** Handler for when the update/edit icon is clicked */
onUpdateClick?: (item: T) => void;
}
/**
* Props for the CardField component
*/
export interface CardFieldProps<T> {
/** Data item the field belongs to */
item: T;
/** Field name to display */
field: string;
/** Current language code */
lang: string;
/** Translations object for field labels */
translations: Record<string, any>;
/** Custom renderer for specific fields */
renderCustomField?: (item: T, field: string) => React.ReactNode;
/** Function to get field values from the item */
getFieldValue: (item: any, field: string) => any;
}
/**
* Props for the CardSkeleton component
*/
export interface CardSkeletonProps {
/** Index of the skeleton in the loading array */
index: number;
/** Fields to create skeleton placeholders for */
showFields: string[];
/** Whether to show a skeleton for the view icon */
showViewIcon: boolean;
/** Whether to show a skeleton for the update/edit icon */
showUpdateIcon: boolean;
}

View File

@ -0,0 +1,46 @@
/**
* Safely gets a field value from an item, supporting nested fields with dot notation
*/
export function getFieldValue(item: any, field: string): any {
if (!item) return "";
// Handle nested fields with dot notation (e.g., "user.name")
if (field.includes(".")) {
const parts = field.split(".");
let value = item;
for (const part of parts) {
if (value === null || value === undefined) return "";
value = value[part];
}
return value;
}
return item[field];
}
/**
* Gets a field label from translations or formats the field name
*/
export function getFieldLabel(field: string, translations: Record<string, any>, lang: string): string {
const t = translations[lang] || {};
return t[field] || field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ");
}
/**
* Generates responsive grid classes based on the gridCols prop
*/
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
const baseClass = "grid grid-cols-1 gap-4";
// Map gridCols to responsive classes
const colClasses: Record<number, string> = {
1: "",
2: "sm:grid-cols-2",
3: "sm:grid-cols-2 md:grid-cols-3",
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"
};
return `${baseClass} ${colClasses[gridCols]}`;
}

View File

@ -0,0 +1,279 @@
"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { CreateComponentProps } from "./types";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
// Import field definitions type
interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string;
}
export function CreateComponent<T>({
refetch,
setMode,
setSelectedItem,
onCancel,
lang,
translations,
formProps = {},
}: CreateComponentProps<T>) {
const t = translations[lang as keyof typeof translations] || {};
// Get field definitions from formProps if available
const fieldDefinitions = formProps.fieldDefinitions || {};
const validationSchema = formProps.validationSchema;
// Group fields by their group property
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
// Process field definitions to group them
useEffect(() => {
if (Object.keys(fieldDefinitions).length > 0) {
const groups: Record<string, FieldDefinition[]> = {};
// Group fields by their group property
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
const def = definition as FieldDefinition;
if (!groups[def.group]) {
groups[def.group] = [];
}
groups[def.group].push({ ...def, name: fieldName });
});
setGroupedFields(groups);
}
}, [fieldDefinitions]);
// Initialize form with default values from field definitions
const defaultValues: Record<string, any> = {};
Object.entries(fieldDefinitions).forEach(([key, def]) => {
const fieldDef = def as FieldDefinition;
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
});
// Setup form with validation schema if available
const {
register,
handleSubmit,
formState: { errors },
setValue,
watch,
} = useForm({
defaultValues,
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
});
const formValues = watch();
// Handle form submission
const onSubmit = async (data: Record<string, any>) => {
try {
console.log("Form data to save:", data);
// Here you would make an API call to save the data
// For example: await createApplication(data);
// Mock API call success
if (refetch) refetch();
setMode("list");
setSelectedItem(null);
} catch (error) {
console.error("Error saving form:", error);
}
};
// Handle select changes
const handleSelectChange = (name: string, value: string) => {
setValue(name, value);
};
// Handle checkbox changes
const handleCheckboxChange = (name: string, checked: boolean) => {
setValue(name, checked);
};
// Translate group names for display dynamically
const getGroupTitle = (groupName: string) => {
// First check if there's a translation for the exact group key
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
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;
};
// Render a field based on its type
const renderField = (fieldName: string, field: FieldDefinition) => {
const errorMessage = errors[fieldName]?.message as string;
switch (field.type) {
case "text":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldName}
{...register(fieldName)}
placeholder={t[fieldName] || field.label}
disabled={field.readOnly}
/>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "textarea":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Textarea
id={fieldName}
{...register(fieldName)}
placeholder={t[fieldName] || field.label}
rows={3}
disabled={field.readOnly}
/>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "select":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Select
value={formValues[fieldName]}
onValueChange={(value) => handleSelectChange(fieldName, value)}
disabled={field.readOnly}
>
<SelectTrigger>
<SelectValue placeholder={t[fieldName] || field.label} />
</SelectTrigger>
<SelectContent>
{field.options?.map((option) => (
<SelectItem key={option} value={option}>
{t[option] || option}
</SelectItem>
))}
</SelectContent>
</Select>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "checkbox":
return (
<div className="flex items-center space-x-2" key={fieldName}>
<Checkbox
id={fieldName}
checked={formValues[fieldName]}
onCheckedChange={(checked) =>
handleCheckboxChange(fieldName, checked as boolean)
}
disabled={field.readOnly}
/>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
default:
return null;
}
};
return (
<form onSubmit={handleSubmit(() => console.log("Form data to save:"))}>
<Card className="w-full mb-6">
<CardHeader>
<CardTitle>{t.create || "Create"}</CardTitle>
<CardDescription>{t.createDescription || "Create a new item"}</CardDescription>
</CardHeader>
<CardContent>
{/* Display validation errors summary if any */}
{Object.keys(errors).length > 0 && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{t.formErrors || "Please correct the errors in the form"}
</AlertDescription>
</Alert>
)}
{/* Render fields grouped by their group property */}
<div className="space-y-6">
{Object.entries(groupedFields).map(([groupName, fields]) => (
<Card key={groupName} className="shadow-sm">
<CardHeader className="py-3">
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{fields.map((field: any) => renderField(field.name, field))}
</div>
</CardContent>
</Card>
))}
</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>
);
}

View File

@ -0,0 +1,120 @@
"use client";
import React, { useEffect, useState } from "react";
import { CreateComponent } from "./CreateComponent";
import { UpdateComponent } from "./UpdateComponent";
import { ViewComponent } from "./ViewComponent";
import { FormDisplayProps } from "./types";
export function FormDisplay<T>({
initialData,
mode,
refetch,
setMode,
setSelectedItem,
onCancel,
lang,
translations,
formProps = {},
}: FormDisplayProps<T>) {
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
// Dynamically import schema definitions if provided in formProps
useEffect(() => {
const loadSchemaDefinitions = async () => {
try {
// Check if schemaPath is provided in formProps
if (formProps.schemaPath) {
// Dynamic import of the schema module
const schemaModule = await import(formProps.schemaPath);
// Get the appropriate field definitions based on mode
let fieldDefs;
if (schemaModule.fieldDefinitions?.getDefinitionsByMode) {
fieldDefs = schemaModule.fieldDefinitions.getDefinitionsByMode(mode);
} else if (mode === "create" && schemaModule.createFieldDefinitions) {
fieldDefs = schemaModule.createFieldDefinitions;
} else if (mode === "update" && schemaModule.updateFieldDefinitions) {
fieldDefs = schemaModule.updateFieldDefinitions;
} else if (mode === "view" && schemaModule.viewFieldDefinitions) {
fieldDefs = schemaModule.viewFieldDefinitions;
}
// Get the appropriate validation schema based on mode
let validationSchema;
if (mode === "create" && schemaModule.CreateApplicationSchema) {
validationSchema = schemaModule.CreateApplicationSchema;
} else if (mode === "update" && schemaModule.UpdateApplicationSchema) {
validationSchema = schemaModule.UpdateApplicationSchema;
} else if (mode === "view" && schemaModule.ViewApplicationSchema) {
validationSchema = schemaModule.ViewApplicationSchema;
} else if (schemaModule.ApplicationSchema) {
validationSchema = schemaModule.ApplicationSchema;
}
// Get the grouped field definitions structure if available
const groupedFieldDefs = schemaModule.baseFieldDefinitions || {};
// Update form props with schema information
setEnhancedFormProps({
...formProps,
fieldDefinitions: fieldDefs || {},
validationSchema,
fieldsByMode: schemaModule.fieldsByMode || {},
groupedFieldDefinitions: groupedFieldDefs,
});
}
} catch (error) {
console.error("Error loading schema definitions:", error);
}
};
loadSchemaDefinitions();
}, [formProps, mode, lang]); // Added lang as a dependency to ensure re-fetch when language changes
// Render the appropriate component based on the mode
switch (mode) {
case "create":
return (
<CreateComponent<T>
key={`create-${lang}`} // Add key with lang to force re-render on language change
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={onCancel}
lang={lang}
translations={translations}
formProps={enhancedFormProps}
/>
);
case "update":
return initialData ? (
<UpdateComponent<T>
key={`update-${lang}`} // Add key with lang to force re-render on language change
initialData={initialData}
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={onCancel}
lang={lang}
translations={translations}
formProps={enhancedFormProps}
/>
) : null;
case "view":
return initialData ? (
<ViewComponent<T>
key={`view-${lang}`} // Add key with lang to force re-render on language change
initialData={initialData}
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={onCancel}
lang={lang}
translations={translations}
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
/>
) : null;
default:
return null;
}
}

View File

@ -0,0 +1,312 @@
"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { UpdateComponentProps } from "./types";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
// Import field definitions type
interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string; // Add name property for TypeScript compatibility
}
export function UpdateComponent<T>({
initialData,
refetch,
setMode,
setSelectedItem,
onCancel,
lang,
translations,
formProps = {},
}: 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;
// Group fields by their group property
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
// Process field definitions to group them
useEffect(() => {
if (Object.keys(fieldDefinitions).length > 0) {
const groups: Record<string, FieldDefinition[]> = {};
// Group fields by their group property
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
const def = definition as FieldDefinition;
if (!groups[def.group]) {
groups[def.group] = [];
}
groups[def.group].push({ ...def, name: fieldName });
});
setGroupedFields(groups);
}
}, [fieldDefinitions]);
// Initialize form with default values from field definitions and initialData
const defaultValues: Record<string, any> = {};
Object.entries(fieldDefinitions).forEach(([key, def]) => {
const fieldDef = def as FieldDefinition;
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
});
// Merge initialData with default values
if (initialData) {
Object.assign(defaultValues, initialData as Record<string, any>);
}
// Setup form with validation schema if available
const {
register,
handleSubmit,
formState: { errors },
setValue,
watch,
reset,
} = useForm({
defaultValues,
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
});
// Reset form when initialData changes
useEffect(() => {
if (initialData) {
reset({ ...initialData as Record<string, any> });
}
}, [initialData, reset]);
const formValues = watch();
// Handle form submission
const onSubmit = async (data: any) => {
try {
console.log("Form data to update:", data);
// Here you would make an API call to update the data
// For example: await updateApplication(data);
// Mock API call success
if (refetch) refetch();
setMode("list");
setSelectedItem(null);
} catch (error) {
console.error("Error updating form:", error);
}
};
// Handle select changes
const handleSelectChange = (name: string, value: string) => {
setValue(name, value);
};
// Handle checkbox changes
const handleCheckboxChange = (name: string, checked: boolean) => {
setValue(name, checked);
};
// Translate group names for display dynamically
const getGroupTitle = (groupName: string) => {
// First check if there's a translation for the exact group key
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
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;
};
// Render a field based on its type
const renderField = (fieldName: string, field: FieldDefinition) => {
const errorMessage = errors[fieldName]?.message as string;
switch (field.type) {
case "text":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldName}
{...register(fieldName)}
placeholder={t[fieldName] || field.label}
disabled={field.readOnly}
/>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "textarea":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Textarea
id={fieldName}
{...register(fieldName)}
placeholder={t[fieldName] || field.label}
rows={3}
disabled={field.readOnly}
/>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "select":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Select
value={formValues[fieldName]}
onValueChange={(value) => handleSelectChange(fieldName, value)}
disabled={field.readOnly}
>
<SelectTrigger>
<SelectValue placeholder={t[fieldName] || field.label} />
</SelectTrigger>
<SelectContent>
{field.options?.map((option) => (
<SelectItem key={option} value={option}>
{t[option] || option}
</SelectItem>
))}
</SelectContent>
</Select>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "checkbox":
return (
<div className="flex items-center space-x-2" key={fieldName}>
<Checkbox
id={fieldName}
checked={formValues[fieldName]}
onCheckedChange={(checked) =>
handleCheckboxChange(fieldName, checked as boolean)
}
disabled={field.readOnly}
/>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
case "date":
return (
<div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}>
{t[fieldName] || field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldName}
type="date"
{...register(fieldName)}
disabled={field.readOnly}
/>
{errorMessage && (
<p className="text-sm text-red-500">{errorMessage}</p>
)}
</div>
);
default:
return null;
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card className="w-full mb-6">
<CardHeader>
<CardTitle>{t.update || "Update"}</CardTitle>
<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">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{t.formErrors || "Please correct the errors in the form"}
</AlertDescription>
</Alert>
)}
{/* Render fields grouped by their group property */}
<div className="space-y-6">
{Object.entries(groupedFields).map(([groupName, fields]) => (
<Card key={groupName} className="shadow-sm">
<CardHeader className="py-3">
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{fields.map((field: any) => renderField(field.name, field))}
</div>
</CardContent>
</Card>
))}
</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>
);
}

View File

@ -0,0 +1,262 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ViewComponentProps } from "./types";
import { Label } from "@/components/ui/label";
import { z } from "zod";
// Import field definitions type
export interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string; // Add name property for TypeScript compatibility
}
// Utility function to format field label
const formatFieldLabel = (fieldName: string) =>
fieldName
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.replace(/^./, (str) => str.toUpperCase())
.replace(/\b\w/g, (c) => c.toUpperCase());
// Component for rendering a single field
const ViewField: React.FC<{
fieldName: string;
value: any;
label: string;
lang: string;
translations: any;
hasError?: string;
}> = ({ fieldName, value, label, lang, translations: t, hasError }) => {
const formatFieldValue = () => {
if (value === undefined || value === null) return "-";
switch (true) {
case typeof value === 'string' && !isNaN(Date.parse(value)):
return new Date(value).toLocaleString(lang === "tr" ? "tr-TR" : "en-US");
case typeof value === 'boolean':
return value ? (
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
{t.yes || "Yes"}
</span>
) : (
<span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700">
{t.no || "No"}
</span>
);
case fieldName === "application_type" && (value === "employee" || value === "occupant"):
return t[value] || value;
default:
return value;
}
};
return (
<div className="space-y-1">
<Label className="text-sm font-medium">
{t[fieldName] || label}
</Label>
<div className={`rounded-md border ${hasError ? 'border-red-500' : 'border-input'} bg-background px-3 py-2 text-sm min-h-[2.5rem] flex items-center`}>
{formatFieldValue()}
</div>
{hasError && (
<p className="text-sm text-red-500">{hasError}</p>
)}
</div>
);
};
// Component for rendering a group of fields
const ViewFieldGroup: React.FC<{
groupName: string;
fields: FieldDefinition[];
initialData: any;
lang: string;
translations: any;
validationErrors: Record<string, string>;
}> = ({ groupName, fields, initialData, lang, translations, validationErrors }) => {
const getGroupTitle = (name: string) => {
return translations[name] || formatFieldLabel(name);
};
return (
<Card className="shadow-sm">
<CardHeader className="py-3">
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{fields.map((field) => {
const fieldName = field.name || "";
const value = initialData ? (initialData as any)[fieldName] : undefined;
const hasError = validationErrors[fieldName];
return (
<ViewField
key={fieldName}
fieldName={fieldName}
value={value}
label={field.label}
lang={lang}
translations={translations}
hasError={hasError}
/>
);
})}
</div>
</CardContent>
</Card>
);
};
// Main ViewComponent
export function ViewComponent<T>({
initialData,
setMode,
setSelectedItem,
onCancel,
lang,
translations,
formProps = {},
}: ViewComponentProps<T>) {
const t = translations[lang as keyof typeof translations] || {};
// Get field definitions and validation schema from formProps if available
const fieldDefinitions = formProps.fieldDefinitions || {};
const validationSchema = formProps.validationSchema as z.ZodObject<any> | undefined;
// Group fields by their group property
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
// State to track validation errors if any
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// Process field definitions to group them
useEffect(() => {
if (Object.keys(fieldDefinitions).length > 0) {
const groups: Record<string, FieldDefinition[]> = {};
// Group fields by their group property
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
const def = definition as FieldDefinition;
if (!groups[def.group]) {
groups[def.group] = [];
}
groups[def.group].push({ ...def, name: fieldName });
});
setGroupedFields(groups);
}
}, [fieldDefinitions]);
// For view mode, we don't need strict validation
// Just log validation issues but don't show errors to the user
useEffect(() => {
if (validationSchema && initialData) {
try {
// Try to parse the data through the Zod schema
validationSchema.parse(initialData);
// Clear any previous validation errors if successful
setValidationErrors({});
} catch (error) {
if (error instanceof z.ZodError) {
// Just log the errors but don't set them in the state
// This prevents showing validation errors in view mode
console.warn('View data validation issues (not shown to user):', error.errors);
// Clear any previous validation errors
setValidationErrors({});
}
}
}
}, [initialData, validationSchema]);
const handleEdit = () => {
setMode("update");
};
return (
<Card className="w-full mb-6">
<CardHeader>
<CardTitle>{t.view || "View"}</CardTitle>
<CardDescription>{t.viewDescription || "View item details"}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* If we have grouped fields from schema, use them */}
{Object.keys(groupedFields).length > 0 ? (
// Render fields grouped by their group property
Object.entries(groupedFields).map(([groupName, fields]) => (
<ViewFieldGroup
key={groupName}
groupName={groupName}
fields={fields}
initialData={initialData}
lang={lang}
translations={t}
validationErrors={validationErrors}
/>
))
) : (
// Fallback to the default view if no field definitions are available
initialData && (
<div className="space-y-4">
{validationSchema ? (
// If we have a validation schema, use its shape to determine fields
Object.entries(validationSchema.shape || {}).map(([fieldName, _]) => {
const value = (initialData as any)[fieldName];
// Skip undefined or null values
if (value === undefined || value === null) return null;
return (
<ViewField
key={fieldName}
fieldName={fieldName}
value={value}
label={formatFieldLabel(fieldName)}
lang={lang}
translations={t}
/>
);
})
) : (
// If no schema, just iterate over the data keys
Object.entries(initialData as Record<string, any>).map(([fieldName, value]) => {
// Skip undefined or null values
if (value === undefined || value === null) return null;
return (
<ViewField
key={fieldName}
fieldName={fieldName}
value={value}
label={formatFieldLabel(fieldName)}
lang={lang}
translations={t}
/>
);
})
)}
</div>
)
)}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="outline" onClick={onCancel}>
{t.back || "Back"}
</Button>
<Button onClick={handleEdit}>
{t.edit || "Edit"}
</Button>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,5 @@
// Export the main components
export { FormDisplay } from "./FormDisplay";
// Export types
export type { FormMode } from "./types";

View File

@ -0,0 +1,37 @@
"use client";
// Define the FormMode type to ensure consistency
export type FormMode = "list" | "create" | "update" | "view";
export interface BaseFormProps<T> {
initialData?: T;
refetch?: () => void;
setMode: React.Dispatch<React.SetStateAction<FormMode>>;
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
onCancel: () => void;
lang: string;
translations: Record<string, Record<string, string>>;
formProps?: Record<string, any>;
}
export interface CreateComponentProps<T> extends BaseFormProps<T> {}
export interface UpdateComponentProps<T> extends BaseFormProps<T> {
initialData: T; // Required for update
}
export interface ViewComponentProps<T> extends BaseFormProps<T> {
initialData: T; // Required for view
}
export interface FormDisplayProps<T> {
mode: FormMode;
initialData?: T;
refetch?: () => void;
setMode: React.Dispatch<React.SetStateAction<FormMode>>;
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
onCancel: () => void;
lang: string;
translations: Record<string, Record<string, string>>;
formProps?: Record<string, any>;
}

View File

@ -0,0 +1,42 @@
"use client";
import React from "react";
export type GridSize = 1 | 2 | 3 | 4 | 5 | 6;
interface GridSelectionComponentProps {
gridCols: GridSize;
setGridCols: (size: GridSize) => void;
translations?: Record<string, any>;
lang?: string;
}
export const GridSelectionComponent: React.FC<GridSelectionComponentProps> = ({
gridCols,
setGridCols,
translations,
lang = "en",
}) => {
const t = translations?.[lang] || {};
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setGridCols(Number(e.target.value) as GridSize);
};
return (
<div className="flex items-center">
<span className="mr-2 text-sm">{t.gridSize || "Grid Size:"}:</span>
<select
value={gridCols}
onChange={handleChange}
className="p-2 border rounded"
>
<option value="1">{t.oneColumn || "1 Column"}</option>
<option value="2">{t.twoColumns || "2 Columns"}</option>
<option value="3">{t.threeColumns || "3 Columns"}</option>
<option value="4">{t.fourColumns || "4 Columns"}</option>
<option value="5">{t.fiveColumns || "5 Columns"}</option>
<option value="6">{t.sixColumns || "6 Columns"}</option>
</select>
</div>
);
};

View File

@ -0,0 +1,37 @@
"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;
}
export const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({
lang,
setLang,
translations,
className = "p-2 border rounded",
}) => {
const t = translations?.[lang] || {};
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setLang(e.target.value as Language);
};
return (
<div className="flex items-center">
<select
value={lang}
onChange={handleChange}
className={className}
>
<option value="en">{t.english || "English"}</option>
<option value="tr">{t.turkish || "Turkish"}</option>
</select>
</div>
);
};

View File

@ -0,0 +1,116 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { PaginationBaseProps } from "./types";
/**
* Calculate the page numbers to display in pagination
* @param currentPage Current active page
* @param totalPages Total number of pages
* @param maxButtons Maximum number of page buttons to show (default: 5)
* @returns Array of page numbers to display
*/
const getPageNumbers = (currentPage: number, totalPages: number, maxButtons: number = 5): number[] => {
const pageNumbers: number[] = [];
// If we have fewer pages than the maximum buttons, show all pages
if (totalPages <= maxButtons) {
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
}
// If we're near the beginning, show first maxButtons pages
else if (currentPage <= 3) {
for (let i = 1; i <= maxButtons; i++) {
pageNumbers.push(i);
}
}
// If we're near the end, show last maxButtons pages
else if (currentPage >= totalPages - 2) {
for (let i = totalPages - maxButtons + 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
}
// Otherwise, show pages centered around current page
else {
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
pageNumbers.push(i);
}
}
return pageNumbers;
};
export const PageNavigation: React.FC<PaginationBaseProps> = ({
pagination,
updatePagination,
loading = false,
lang,
translations,
}) => {
const t = translations[lang] || {};
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= pagination.totalPages) {
updatePagination({ page: newPage });
}
};
// Get the page numbers to display
const pageNumbers = getPageNumbers(pagination.page, pagination.totalPages);
return (
<div className="flex items-center space-x-2">
{pagination.back ? (
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(pagination.page - 1)}
disabled={loading}
>
{t.previous || "Previous"}
</Button>
) : (
<Button variant="ghost" size="sm" disabled>
{t.previous || "Previous"}
</Button>
)}
{/* Page number buttons */}
<div className="flex items-center space-x-1">
{pageNumbers.map((pageNum) => (
<Button
key={pageNum}
variant={pagination.page === pageNum ? "default" : "outline"}
size="sm"
className="w-9 h-9 p-0"
onClick={() => handlePageChange(pageNum)}
disabled={loading}
>
{pageNum}
</Button>
))}
</div>
{pagination.page < pagination.totalPages ? (
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(pagination.page + 1)}
disabled={loading}
>
{t.next || "Next"}
</Button>
) : (
<Button variant="ghost" size="sm" disabled>
{t.next || "Next"}
</Button>
)}
{/* Page text display */}
<span className="px-4 py-1 text-sm text-muted-foreground">
{t.page || "Page"} {pagination.page} {t.of || "of"} {pagination.totalPages}
</span>
</div>
);
};

View File

@ -0,0 +1,52 @@
"use client";
import React from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PaginationBaseProps } from "./types";
interface PageSizeSelectorProps extends PaginationBaseProps {
pageSizeOptions?: number[];
}
export const PageSizeSelector: React.FC<PageSizeSelectorProps> = ({
pagination,
updatePagination,
lang,
translations,
pageSizeOptions = [5, 10, 20, 50],
}) => {
const t = translations[lang] || {};
return (
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">
{t.itemsPerPage || "Items per page"}
</span>
<Select
value={pagination.size.toString()}
onValueChange={(value) => {
updatePagination({
size: Number(value),
page: 1, // Reset to first page when changing page size
});
}}
>
<SelectTrigger className="w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
{pageSizeOptions.map((size) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};

View File

@ -0,0 +1,36 @@
"use client";
import React from "react";
import { PaginationBaseProps } from "./types";
export const PaginationStats: React.FC<PaginationBaseProps> = ({
pagination,
lang,
translations,
}) => {
const t = translations[lang] || {};
return (
<div className="text-sm text-muted-foreground">
<div>
{t.showing || "Showing"}{" "}
{/* Show the range based on filtered count when available */}
{(pagination.totalCount || pagination.allCount || 0) > 0
? (pagination.page - 1) * pagination.size + 1
: 0}{" "}
-{" "}
{Math.min(
pagination.page * pagination.size,
pagination.totalCount || pagination.allCount || 0
)}{" "}
{t.of || "of"} {pagination.totalCount || pagination.allCount || 0} {t.items || "items"}
</div>
{pagination.totalCount &&
pagination.totalCount !== (pagination.allCount || 0) && (
<div>
{t.total || "Total"}: {pagination.allCount || 0} {t.items || "items"} ({t.filtered || "Filtered"}:{" "}
{pagination.totalCount} {t.items || "items"})
</div>
)}
</div>
);
};

View File

@ -0,0 +1,43 @@
"use client";
import React from "react";
import { PaginationBaseProps } from "./types";
import { PaginationStats } from "./PaginationStats";
import { PageNavigation } from "./PageNavigation";
import { PageSizeSelector } from "./PageSizeSelector";
export const PaginationToolsComponent: React.FC<PaginationBaseProps> = ({
pagination,
updatePagination,
loading = false,
lang,
translations,
}) => {
return (
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
{/* Pagination stats - left side */}
<PaginationStats
pagination={pagination}
updatePagination={updatePagination}
lang={lang}
translations={translations}
/>
{/* Navigation buttons - center */}
<PageNavigation
pagination={pagination}
updatePagination={updatePagination}
loading={loading}
lang={lang}
translations={translations}
/>
{/* Items per page selector - right side */}
<PageSizeSelector
pagination={pagination}
updatePagination={updatePagination}
lang={lang}
translations={translations}
/>
</div>
);
};

View File

@ -0,0 +1,5 @@
export * from './PaginationToolsComponent';
export * from './PaginationStats';
export * from './PageNavigation';
export * from './PageSizeSelector';
export * from './types';

View File

@ -0,0 +1,19 @@
import { PagePagination } from "../hooks/useDataFetching";
export interface ResponseMetadata {
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
allCount?: number;
next: boolean;
back: boolean;
}
export interface PaginationBaseProps {
pagination: PagePagination;
updatePagination: (updates: Partial<PagePagination>) => void;
loading?: boolean;
lang: string;
translations: Record<string, any>;
}

View File

@ -0,0 +1,67 @@
"use client";
import React, { useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { SelectQueryModifierProps } from "./types";
export const SelectQueryModifier: React.FC<SelectQueryModifierProps> = ({
fieldKey,
value,
label,
options,
placeholder,
onQueryChange,
translations,
lang,
}) => {
const t = translations[lang] || {};
const handleChange = useCallback((newValue: string) => {
const formattedValue = newValue.trim() ? `%${newValue.trim()}%` : null;
onQueryChange(`${fieldKey}__ilike`, formattedValue);
}, [fieldKey, onQueryChange]);
const handleClear = useCallback(() => {
onQueryChange(fieldKey, null);
onQueryChange(`${fieldKey}__ilike`, null);
}, [fieldKey, onQueryChange]);
return (
<div className="w-full">
<div className="flex justify-between items-center">
<label className="block text-xs font-medium">
{label || t[fieldKey] || fieldKey}
</label>
{value && (
<Button
variant="ghost"
size="sm"
className="h-5 px-2 text-xs"
onClick={handleClear}
>
{t.clear || "Clear"}
</Button>
)}
</div>
<div className="w-full mt-1">
<Select
value={value}
onValueChange={handleChange}
>
<SelectTrigger className="w-full h-10">
<SelectValue
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
/>
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@ -0,0 +1,80 @@
"use client";
import React, { useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, X } from "lucide-react";
import { TextQueryModifierProps } from "./types";
export const TextQueryModifier: React.FC<TextQueryModifierProps> = ({
fieldKey,
value,
label,
placeholder,
onQueryChange,
translations,
lang,
}) => {
const t = translations[lang] || {};
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onQueryChange(fieldKey, newValue);
}, [fieldKey, onQueryChange]);
const handleClear = useCallback(() => {
// Clear both the regular field and the ilike filter
onQueryChange(fieldKey, null);
onQueryChange(`${fieldKey}__ilike`, null);
}, [fieldKey, onQueryChange]);
const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
// Apply the search immediately on Enter
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
onQueryChange(`${fieldKey}__ilike`, formattedValue);
}
}, [fieldKey, value, onQueryChange]);
const handleSearch = useCallback(() => {
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
onQueryChange(`${fieldKey}__ilike`, formattedValue);
}, [fieldKey, value, onQueryChange]);
return (
<div className="w-full">
<label className="block text-xs font-medium mb-1">
{label || t[fieldKey] || fieldKey}
</label>
<div className="relative w-full flex">
<div className="relative w-full">
<Input
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
value={value || ""}
onChange={handleChange}
onKeyUp={handleKeyUp}
className="pl-8 pr-8 w-full h-10"
/>
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
{value && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1 h-8 w-8"
onClick={handleClear}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<Button
variant="default"
size="sm"
className="ml-2"
onClick={handleSearch}
>
<Search className="h-4 w-4" />
</Button>
</div>
</div>
);
};

View File

@ -0,0 +1,51 @@
"use client";
import React, { useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { User } from "lucide-react";
import { TypeQueryModifierProps } from "./types";
export const TypeQueryModifier: React.FC<TypeQueryModifierProps & { defaultValue?: string }> = ({
fieldKey,
value,
options,
onQueryChange,
translations,
lang,
defaultValue,
}) => {
const t = translations[lang] || {};
const handleTypeSelect = useCallback((selectedValue: string) => {
const formattedValue = selectedValue.trim() ? `%${selectedValue.trim()}%` : null;
onQueryChange(`${fieldKey}__ilike`, formattedValue);
}, [fieldKey, onQueryChange]);
// Apply default value on initial render if no value is set
useEffect(() => {
if (defaultValue && !value && options.some(opt => opt.value === defaultValue)) {
handleTypeSelect(defaultValue);
}
}, [defaultValue, value, options, handleTypeSelect]);
if (!options || options.length === 0) return null;
return (
<div className="w-full flex flex-col space-y-4">
<div className="font-medium text-sm flex items-center">
<User className="mr-2 h-4 w-4" />
{t.typeSelection || "Type Selection"}
</div>
{options?.map((option) => (
<Button
key={option.value}
variant={value === option.value ? "default" : "outline"}
className="justify-start"
onClick={() => handleTypeSelect(option.value)}
>
{option.icon}
{option.label}
</Button>
))}
</div>
);
};

View File

@ -0,0 +1,4 @@
export * from './types';
export * from './TextQueryModifier';
export * from './SelectQueryModifier';
export * from './TypeQueryModifier';

View File

@ -0,0 +1,35 @@
import { ReactNode } from 'react';
export interface QueryModifierProps {
onQueryChange: (key: string, value: string | null) => void;
translations: Record<string, Record<string, string>>;
lang: string;
}
export interface TextQueryModifierProps extends QueryModifierProps {
fieldKey: string;
value: string;
label: string;
placeholder?: string;
}
export interface SelectQueryModifierProps extends QueryModifierProps {
fieldKey: string;
value: string;
label: string;
options: { value: string; label: string }[];
placeholder?: string;
}
export interface TypeQueryModifierProps extends QueryModifierProps {
fieldKey: string;
value: string;
options: { value: string; label: string; icon?: React.ReactNode }[];
}
export interface QueryModifierValue {
key: string;
value: string;
}
export type QueryModifierResult = Record<string, string>;

View File

@ -0,0 +1,128 @@
# EVYOS Management Frontend - Common Components
## Overview
This directory contains modular, reusable components for building consistent UIs across the EVYOS Management Frontend. These components follow a modular design pattern where complex functionality is broken down into smaller, focused components with clear responsibilities.
## Component Structure
### ActionButtonsDisplay
- **CreateButton**: A button component for triggering create actions with translation support
- **CustomButtonComponent**: Configurable button component with selection state support
- **types.ts**: Shared type definitions for button components
### CardDisplay
- **CardDisplay**: Main component for displaying data in a responsive grid layout
- **CardItem**: Individual card component with customizable fields and actions
- **CardSkeleton**: Loading state placeholder for cards
- **schema.ts**: API response and data schemas
- **utils.ts**: Helper functions for card operations
### FormDisplay
- **FormDisplay**: Container component that handles form mode switching
- **CreateComponent**: Form implementation for creating new records
- **UpdateComponent**: Form implementation for updating existing records
- **ViewComponent**: Read-only view of record details
- **types.ts**: Type definitions for form components and modes
### HeaderSelections
- **GridSelectionComponent**: Controls the number of columns in card grid layouts
- **LanguageSelectionComponent**: Language switcher with translation support
### PaginationModifiers
- **PaginationToolsComponent**: Main container for pagination controls
- **PaginationStats**: Displays record count information
- **PageNavigation**: Handles page navigation buttons with smart page number calculation
- **PageSizeSelector**: Controls items per page selection
- **types.ts**: Type definitions including ResponseMetadata interface
### QueryModifiers
- **TextQueryModifier**: Text search input with clear functionality
- **SelectQueryModifier**: Dropdown selection for filtering
- **TypeQueryModifier**: Button-based type selection
- **types.ts**: Shared interfaces for query components
### Hooks
- **useApiData**: Custom hook for fetching and managing API data with pagination
- **useDataFetching**: Base hook for data fetching with pagination, sorting, and filtering
## Usage Example
```tsx
// Import components
import { CardDisplay } from "@/components/common/CardDisplay";
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
import { useApiData } from "@/components/common/hooks/useApiData";
// Use the API data hook
const {
data,
pagination,
loading,
error,
updatePagination,
refetch
} = useApiData<YourDataType>("/api/your-endpoint");
// Define fields to display
const showFields = ["field1", "field2", "field3"];
// Example component usage
<Card>
<CardContent>
<TextQueryModifier
fieldKey="name"
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
label="Search"
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
</CardContent>
</Card>
<CardDisplay
showFields={showFields}
data={data}
lang={lang}
translations={translations}
error={error}
loading={loading}
titleField="name"
onCardClick={handleCardClick}
gridCols={gridCols}
showViewIcon={true}
showUpdateIcon={true}
onViewClick={handleViewClick}
onUpdateClick={handleUpdateClick}
/>
```
## API Response Structure
Components expect API responses in this format:
```typescript
interface ApiResponse<T> {
data: T[];
pagination: {
page: number;
size: number;
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
allCount?: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
next: boolean;
back: boolean;
};
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,49 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { ArrowDown, ArrowUp } from "lucide-react";
import { SortingComponentProps, SortField } from "./types";
export const SortingComponent: React.FC<SortingComponentProps> = ({
sortField,
sortDirection,
onSort,
translations,
lang,
sortFields = [
{ key: "name", label: "Name" },
{ key: "code", label: "Code" },
{ key: "type", label: "Type" },
{ key: "created_at", label: "Created" },
],
}) => {
const t = translations?.[lang] || {};
return (
<div className="flex flex-wrap gap-2 my-4">
<div className="text-sm font-medium mr-2 flex items-center">
{t.sortBy || "Sort by:"}
</div>
{sortFields.map((field) => (
<Button
key={field.key}
variant={sortField === field.key ? "default" : "outline"}
size="sm"
onClick={() => onSort(field.key)}
className="flex items-center"
>
{t[field.key] || field.label}
{sortField === field.key && (
<>
{sortDirection === "asc" ? (
<ArrowUp className="ml-1 h-3 w-3" />
) : (
<ArrowDown className="ml-1 h-3 w-3" />
)}
</>
)}
</Button>
))}
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './SortingComponent';
export * from './types';

View File

@ -0,0 +1,13 @@
export interface SortField {
key: string;
label: string;
}
export interface SortingComponentProps {
sortField: string | null;
sortDirection: "asc" | "desc" | null;
onSort: (field: string) => void;
translations?: Record<string, any>;
lang: string;
sortFields?: SortField[];
}

View File

@ -0,0 +1,68 @@
import { useDataFetching, ApiResponse } from "./useDataFetching";
import { RequestParams } from "../schemas";
/**
* Hook for fetching data from Next.js API routes
* @param endpoint The API endpoint to fetch data from (e.g., '/api/applications')
* @param initialParams Initial request parameters
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
*/
export function useApiData<T>(
endpoint: string,
initialParams: Partial<RequestParams> = {}
) {
// Define the fetch function that will be passed to useDataFetching
const fetchFromApi = async (
params: RequestParams
): Promise<ApiResponse<T>> => {
try {
// Prepare the request body with action and all params
const requestBody = {
action: "list",
page: params.page,
size: params.size,
orderField: params.orderField,
orderType: params.orderType,
query: params.query,
};
// Make the API request using POST
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching data from API:", error);
// Return empty data with pagination info on error
return {
data: [],
pagination: {
page: params.page,
size: params.size,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: params.orderField,
orderType: params.orderType,
query: params.query,
next: false,
back: false,
},
};
}
};
// Use the generic data fetching hook with our API-specific fetch function
return useDataFetching<T>(fetchFromApi, initialParams);
}

View File

@ -0,0 +1,117 @@
import { retrievePagebyUrl, retrievePageList } from "@/apicalls/cookies/token";
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
import React from "react";
export interface DashboardPageParams {
/**
* The active page path, e.g., "/individual", "/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: any;
/**
* The list of site URLs for the menu
*/
siteUrlsList: string[];
}
/**
* Hook to retrieve and prepare dashboard page data for client-frontend application
* Handles retrieving site URLs, page components, and other necessary data
*
* @param params The dashboard page parameters
* @returns The processed dashboard page data including site URLs
* @throws Error if page URL is invalid or page component is not found
*/
export async function useDashboardPage({
pageUrl,
searchParams,
}: 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 site URLs list
let siteUrlsList: string[] = [];
try {
siteUrlsList = (await retrievePageList()) || [];
} catch (err) {
console.error("Error retrieving site URLs:", err);
throw new Error(`Failed to retrieve site URLs: ${err}`);
}
// Get page component
let pageToDirect;
let PageComponent;
try {
pageToDirect = await retrievePagebyUrl(pageUrl);
PageComponent = retrievePageByUrlAndPageId(pageToDirect, pageUrl);
} catch (err) {
console.error("Error retrieving page component:", err);
throw new Error(`Page component not found for URL: ${pageUrl}`);
}
// Check if page component exists
if (!PageComponent) {
throw new Error(`Page component not found for URL: ${pageUrl}`);
}
return {
activePage: pageUrl,
searchParamsInstance,
lang,
PageComponent,
siteUrlsList,
};
}
export default useDashboardPage;

View File

@ -0,0 +1,178 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { RequestParams, ResponseMetadata } from "../schemas";
export interface PagePagination extends RequestParams, ResponseMetadata {}
export interface ApiResponse<T> {
data: T[];
pagination: PagePagination;
}
/**
* Generic data fetching hook that can be used with any API endpoint
* @param fetchFunction - The API function to call for fetching data
* @param initialParams - Initial request parameters
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
*/
export function useDataFetching<T>(
fetchFunction: (params: RequestParams) => Promise<ApiResponse<T>>,
initialParams: Partial<RequestParams> = {}
) {
const [data, setData] = useState<T[]>([]);
// Request parameters - these are controlled by the user
const [requestParams, setRequestParams] = useState<RequestParams>({
page: initialParams.page || 1,
size: initialParams.size || 10,
orderField: initialParams.orderField || ["name"],
orderType: initialParams.orderType || ["asc"],
query: initialParams.query || {},
});
// Response metadata - these come from the API
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
next: true,
back: false,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchDataFromApi = useCallback(async () => {
setLoading(true);
try {
const result = await fetchFunction({
page: requestParams.page,
size: requestParams.size,
orderField: requestParams.orderField,
orderType: requestParams.orderType,
query: requestParams.query,
});
if (result && result.data) {
setData(result.data);
// Update response metadata from API response
if (result.pagination) {
setResponseMetadata({
totalCount: result.pagination.totalCount || 0,
totalItems: result.pagination.totalCount || 0,
totalPages: result.pagination.totalPages || 1,
pageCount: result.pagination.pageCount || 0,
allCount: result.pagination.allCount || 0,
next: result.pagination.next || false,
back: result.pagination.back || false,
});
}
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setLoading(false);
}
}, [
fetchFunction,
requestParams.page,
requestParams.size,
requestParams.orderField,
requestParams.orderType,
requestParams.query,
]);
// Track if this is the initial mount
const initialMountRef = useRef(true);
// Track previous request params to avoid unnecessary fetches
const prevRequestParamsRef = useRef<RequestParams>(requestParams);
useEffect(() => {
// Only fetch on mount or when request params actually change
const paramsChanged =
JSON.stringify(prevRequestParamsRef.current) !==
JSON.stringify(requestParams);
if (initialMountRef.current || paramsChanged) {
const timer = setTimeout(() => {
fetchDataFromApi();
initialMountRef.current = false;
prevRequestParamsRef.current = { ...requestParams };
}, 300); // Debounce
return () => clearTimeout(timer);
}
}, [fetchDataFromApi, requestParams]);
const updatePagination = useCallback((updates: Partial<RequestParams>) => {
// Transform query parameters to use __ilike with %value% format
if (updates.query) {
const transformedQuery: Record<string, any> = {};
Object.entries(updates.query).forEach(([key, value]) => {
// Only transform string values that aren't already using a special operator
if (
typeof value === "string" &&
!key.includes("__") &&
value.trim() !== ""
) {
transformedQuery[`${key}__ilike`] = `%${value}%`;
} else {
transformedQuery[key] = value;
}
});
updates.query = transformedQuery;
// Always reset to page 1 when search query changes
if (!updates.hasOwnProperty("page")) {
updates.page = 1;
}
// Reset response metadata when search changes to avoid stale pagination data
setResponseMetadata({
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
allCount: 0,
next: true,
back: false,
});
}
setRequestParams((prev) => ({
...prev,
...updates,
}));
}, []);
// Create a combined refetch function
const refetch = useCallback(() => {
// Reset pagination to page 1 when manually refetching
setRequestParams((prev) => ({
...prev,
page: 1,
}));
fetchDataFromApi();
}, [fetchDataFromApi]);
// Combine request params and response metadata
const pagination: PagePagination = {
...requestParams,
...responseMetadata,
};
return {
data,
pagination,
loading,
error,
updatePagination,
refetch,
};
}

View File

@ -0,0 +1,181 @@
import { useState, useEffect } from 'react';
/**
* Cache options for the fetch request
*/
export type CacheOptions = {
/** Whether to cache the request (default: true) */
cache?: boolean;
/** Revalidate time in seconds (if not provided, uses Next.js defaults) */
revalidate?: number;
/** Force cache to be revalidated (equivalent to cache: 'no-store' in fetch) */
noStore?: boolean;
};
/**
* Request options for the fetch
*/
export type FetchOptions = {
/** HTTP method (default: 'GET') */
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/** Request headers */
headers?: HeadersInit;
/** Request body (for POST, PUT, PATCH) */
body?: any;
};
/**
* A hook for fetching data from an API endpoint without pagination using Next.js fetch
* @param url The API endpoint URL
* @param initialParams Initial query parameters
* @param options Additional fetch options
* @param cacheOptions Cache control options
* @returns Object containing data, loading state, error state, and refetch function
*/
export function useStandardApiFetch<T>(
url: string,
initialParams: Record<string, any> = {},
options: FetchOptions = {},
cacheOptions: CacheOptions = { cache: true }
) {
const [data, setData] = useState<T | null>(null);
const [params, setParams] = useState<Record<string, any>>(initialParams);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
/**
* Builds the URL with query parameters
*/
const buildUrl = () => {
const queryParams = new URLSearchParams();
// Add all non-null and non-empty params
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== '') {
queryParams.append(key, String(value));
}
});
const queryString = queryParams.toString();
return queryString ? `${url}?${queryString}` : url;
};
/**
* Configure fetch options including cache settings
*/
const getFetchOptions = (): RequestInit => {
const { method = 'GET', headers = {}, body } = options;
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
};
// Add body for non-GET requests if provided
if (method !== 'GET' && body) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
// Configure cache options
if (!cacheOptions.cache) {
fetchOptions.cache = 'no-store';
} else if (cacheOptions.noStore) {
fetchOptions.cache = 'no-store';
} else if (cacheOptions.revalidate !== undefined) {
fetchOptions.next = { revalidate: cacheOptions.revalidate };
}
return fetchOptions;
};
const fetchData = async () => {
setLoading(true);
try {
const fullUrl = buildUrl();
const fetchOptions = getFetchOptions();
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const responseData = await response.json();
setData(responseData);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An unknown error occurred'));
setData(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url, JSON.stringify(params), JSON.stringify(options), JSON.stringify(cacheOptions)]);
/**
* Update the query parameters and trigger a refetch
* @param newParams New parameters to merge with existing ones
*/
const updateParams = (newParams: Record<string, any>) => {
// Filter out null or empty string values
const filteredParams = Object.entries(newParams).reduce((acc, [key, value]) => {
if (value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, any>);
setParams(prev => ({
...prev,
...filteredParams
}));
};
/**
* Reset all parameters to initial values
*/
const resetParams = () => {
setParams(initialParams);
};
/**
* Manually trigger a refetch of the data
*/
const refetch = () => {
fetchData();
};
return {
data,
loading,
error,
updateParams,
resetParams,
refetch
};
}
// // Basic usage (with default caching)
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>('/api/your-endpoint');
// // With no caching (for data that changes frequently)
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
// '/api/your-endpoint',
// {},
// {},
// { cache: false }
// );
// // With specific revalidation time
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
// '/api/your-endpoint',
// {},
// {},
// { revalidate: 60 } // Revalidate every 60 seconds
// );

View File

@ -0,0 +1,26 @@
// Export all components from the common directory
export { CardDisplay } from "./CardDisplay";
export { SortingComponent, type SortingComponentProps, type SortField } from "./SortingComponent";
// Export QueryModifiers
export {
TextQueryModifier,
SelectQueryModifier,
TypeQueryModifier,
type QueryModifierProps,
type TextQueryModifierProps,
type SelectQueryModifierProps,
type TypeQueryModifierProps,
type QueryModifierValue,
type QueryModifierResult
} from "./QueryModifiers";
// Export hooks
export {
useDataFetching,
type RequestParams,
type ResponseMetadata,
type PagePagination,
type ApiResponse,
} from "./hooks/useDataFetching";
export { useApiData } from "./hooks/useApiData";

View File

@ -0,0 +1,37 @@
// Carried schemas from any request and response
// Common request parameters interface
export interface RequestParams {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
// Common response metadata interface
export interface ResponseMetadata {
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
allCount?: number;
next: boolean;
back: boolean;
}
// Generic API response interface
export interface ApiResponse<T> {
data: T[];
metadata: ResponseMetadata;
}
// Pagination state interface
export interface PagePagination {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}

View File

@ -236,9 +236,8 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
notifications.map((notification) => ( notifications.map((notification) => (
<div <div
key={notification.id} key={notification.id}
className={`px-4 py-2 border-b last:border-b-0 ${ className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
!notification.read ? "bg-blue-50" : "" }`}
}`}
> >
<div className="flex justify-between"> <div className="flex justify-between">
<h4 className="text-sm font-semibold"> <h4 className="text-sm font-semibold">
@ -315,9 +314,8 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
messages.map((message) => ( messages.map((message) => (
<div <div
key={message.id} key={message.id}
className={`px-4 py-2 border-b last:border-b-0 ${ className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
!message.read ? "bg-blue-50" : "" }`}
}`}
> >
<div className="flex items-start"> <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"> <div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">

View File

@ -0,0 +1,44 @@
"use client";
import React, { ReactNode } from "react";
import Header from "@/components/header/Header";
import ClientMenu from "@/components/menu/menu";
interface DashboardLayoutProps {
children: ReactNode;
lang: "en" | "tr";
activePage: string;
siteUrls: string[];
}
/**
* A reusable dashboard layout component that provides consistent structure
* for all dashboard pages with sidebar, header, and content area.
*/
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
lang,
activePage,
siteUrls,
}) => {
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} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4 overflow-y-auto">
{/* Header Component */}
<Header lang={lang} />
{/* Page Content */}
<div className="container mx-auto p-4">
{children}
</div>
</div>
</div>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,155 @@
"use client";
import React, { ReactNode, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
import type { FormMode } from "@/components/common/FormDisplay/types";
interface PageTemplateProps {
title: string;
lang: "en" | "tr";
translations: Record<string, any>;
// Search section
searchSection?: ReactNode;
// Data and pagination
data: any[];
pagination: any;
updatePagination: (params: any) => void;
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;
}
/**
* A reusable page template that follows the modular pattern established
* in the card-example page refactoring.
*/
export const PageTemplate: React.FC<PageTemplateProps> = ({
title,
lang,
translations,
searchSection,
data,
pagination,
updatePagination,
loading,
error,
refetch,
contentDisplay,
formComponent,
mode,
setMode,
handleCreateClick,
handleCancel,
setLang,
headerActions,
additionalActions,
}) => {
const [gridCols, setGridCols] = useState<GridSize>(3);
return (
<>
{mode === "list" ? (
<>
{/* Header section with title and selection components */}
<div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">{title}</h1>
<div className="flex space-x-4">
{/* Grid Selection */}
<GridSelectionComponent
gridCols={gridCols}
setGridCols={setGridCols}
/>
{/* Language Selection */}
<LanguageSelectionComponent
lang={lang as Language}
translations={translations}
setLang={setLang || (() => { })}
/>
{/* Additional header actions */}
{headerActions}
</div>
</div>
{/* Search filters */}
{searchSection && (
<Card className="mb-4">
<CardContent className="pt-6">
{searchSection}
</CardContent>
</Card>
)}
{/* Create Button */}
<Card className="my-4">
<CardContent className="pt-6">
<CreateButton
onClick={handleCreateClick}
translations={translations}
lang={lang}
/>
{/* Additional action buttons */}
{additionalActions}
</CardContent>
</Card>
{/* Pagination Tools Component */}
<Card className="my-4">
<CardContent className="pt-6">
<PaginationToolsComponent
pagination={pagination}
updatePagination={updatePagination}
loading={loading}
lang={lang}
translations={translations}
/>
</CardContent>
</Card>
{/* Content Display */}
<div className="mt-6">
{contentDisplay}
</div>
</>
) : (
/* Form Display for create/update/view modes */
formComponent || (
<div className="p-4 bg-gray-100 rounded-md">
<p>Form component not provided</p>
<button
onClick={handleCancel}
className="mt-4 px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300"
>
{translations[lang].cancel || "Cancel"}
</button>
</div>
)
)}
</>
);
};
export default PageTemplate;

View File

@ -0,0 +1,271 @@
# Modular Dashboard Layout System
This directory contains reusable layout components for building dashboard pages with a consistent structure and appearance.
## Components
### DashboardLayout
The `DashboardLayout` component provides the overall page structure with:
- Sidebar navigation
- Header
- Main content area
### PageTemplate
The `PageTemplate` component provides a standardized structure for page content with:
- Header section with title and selection components
- Search filters section
- Action buttons section
- Pagination tools section
- Content display section
- Form display for create/update/view modes
## Usage Example
Here's an example of how to use these components to create a new dashboard page:
```tsx
"use client";
import React, { useState } from "react";
import { useApiData } from "@/components/common/hooks/useApiData";
import { CardDisplay } from "@/components/common/CardDisplay";
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
import { Card, CardContent } from "@/components/ui/card";
import { Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import { FormMode } from "@/components/common/FormDisplay/types";
import { PageTemplate } from "@/components/layouts/PageTemplate";
import type { GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
// Import your schema and translations
import * as schema from "./schema";
import { translations } from "./language";
const ExamplePage: React.FC<{ lang: "en" | "tr", queryParams: any }> = ({
lang = "en",
queryParams
}) => {
// API data hook
const {
data,
pagination,
loading,
error,
updatePagination,
refetch
} = useApiData<schema.ExampleData>('/api/examples');
// State management
const [mode, setMode] = useState<FormMode>("list");
const [selectedItem, setSelectedItem] = useState<schema.ExampleData | null>(null);
const [gridCols, setGridCols] = useState<GridSize>(3);
const [currentLang, setCurrentLang] = useState<Language>(lang as Language);
// Fields to display in cards
const showFields = ["name", "type", "status"];
// Query handling
const handleQueryChange = (key: string, value: string | null) => {
const newQuery = { ...pagination.query };
if (value === null || value.trim() === "") {
delete newQuery[key];
} else {
newQuery[key] = value;
}
updatePagination({
page: 1,
query: newQuery,
});
};
// Reset all filters
const handleResetAllFilters = () => {
updatePagination({
page: 1,
query: {},
});
};
// Action handlers
const handleCardClick = (item: schema.ExampleData) => {
console.log("Card clicked:", item);
};
const handleViewClick = (item: schema.ExampleData) => {
setSelectedItem(item);
setMode("view");
};
const handleUpdateClick = (item: schema.ExampleData) => {
setSelectedItem(item);
setMode("update");
};
const handleCreateClick = () => {
setSelectedItem(null);
setMode("create");
};
const handleCancel = () => {
setMode("list");
setSelectedItem(null);
};
// Search section component
const SearchSection = (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
{/* Type selector */}
<TypeQueryModifier
fieldKey="type"
value={pagination.query["type"] || ""}
options={[
{ value: "type1", label: translations[lang].type1 },
{ value: "type2", label: translations[lang].type2 }
]}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
</div>
<div className="flex space-x-4">
{/* Text search */}
<TextQueryModifier
fieldKey="name"
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
label={translations[lang].search || "Search"}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
{/* Status dropdown */}
<SelectQueryModifier
fieldKey="status"
value={pagination.query["status"] || ""}
label={translations[lang].status || "Status"}
options={[
{ value: "active", label: translations[lang].active || "Active" },
{ value: "inactive", label: translations[lang].inactive || "Inactive" }
]}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
</div>
{/* Reset filters button */}
<div className="flex justify-end">
<button
onClick={handleResetAllFilters}
className="px-4 py-2 bg-gray-100 rounded-md hover:bg-gray-200"
>
{translations[lang].resetAll || "Reset All"}
</button>
</div>
</div>
);
// Form component
const FormComponent = selectedItem || mode === "create" ? (
<FormDisplay<schema.ExampleData>
initialData={selectedItem || undefined}
mode={mode}
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={handleCancel}
lang={lang}
translations={translations}
formProps={{
fieldDefinitions: mode === 'create' ? schema.createFieldDefinitions :
mode === 'update' ? schema.updateFieldDefinitions :
schema.viewFieldDefinitions,
validationSchema: mode === 'create' ? schema.CreateExampleSchema :
mode === 'update' ? schema.UpdateExampleSchema :
schema.ViewExampleSchema,
fieldsByMode: schema.fieldsByMode
}}
/>
) : null;
// Content display component
const ContentDisplay = (
<CardDisplay
showFields={showFields}
data={data}
lang={lang}
translations={translations}
error={error}
loading={loading}
titleField="name"
onCardClick={handleCardClick}
gridCols={gridCols}
showViewIcon={true}
showUpdateIcon={true}
onViewClick={handleViewClick}
onUpdateClick={handleUpdateClick}
/>
);
return (
<PageTemplate
title={translations[lang].examplePageTitle || "Example Page"}
lang={lang}
translations={translations}
searchSection={SearchSection}
data={data}
pagination={pagination}
updatePagination={updatePagination}
loading={loading}
error={error}
refetch={refetch}
contentDisplay={ContentDisplay}
formComponent={FormComponent}
mode={mode}
setMode={setMode}
handleCreateClick={handleCreateClick}
handleCancel={handleCancel}
setLang={setCurrentLang}
/>
);
};
export default ExamplePage;
```
## Integration with Next.js App Router
To use these components with Next.js App Router, update your page component like this:
```tsx
// src/app/(DashboardLayout)/your-page/page.tsx
import React from "react";
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
import DashboardLayout from "@/components/layouts/DashboardLayout";
async function YourPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | undefined }>;
}) {
const activePage = "/your-page";
const searchParamsInstance = await searchParams;
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
const PageComponent = retrievePageByUrl(activePage);
return (
<DashboardLayout lang={lang} activePage={activePage}>
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</DashboardLayout>
);
}
export default YourPage;
```
This modular approach makes it easy to create new dashboard pages with consistent structure and behavior.

View File

@ -7,9 +7,10 @@ import type { LanguageTranslation } from "./handler";
interface NavigationMenuProps { interface NavigationMenuProps {
transformedMenu: any[]; transformedMenu: any[];
lang: string; lang: string;
activePage?: string;
} }
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }) => { const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang, activePage }) => {
// State to track which menu items are expanded // State to track which menu items are expanded
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1); const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1); const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
@ -77,16 +78,23 @@ const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }
{firstIndex === firstLayerIndex && {firstIndex === firstLayerIndex &&
secondIndex === secondLayerIndex && ( secondIndex === secondLayerIndex && (
<div className="bg-gray-100 border-t border-gray-200"> <div className="bg-gray-100 border-t border-gray-200">
{subItem.subList.map((subSubItem: any) => ( {subItem.subList.map((subSubItem: any) => {
<Link // Check if this is the active page
key={subSubItem.name} const isActive = activePage === subSubItem.siteUrl;
href={subSubItem.siteUrl}
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors" return (
> <Link
<Home className="h-4 w-4 mr-2 text-gray-500" /> key={subSubItem.name}
<span>{subSubItem.lg[lang]}</span> href={subSubItem.siteUrl}
</Link> className={`flex items-center w-full p-3 pl-12 text-left transition-colors ${isActive
))} ? "bg-emerald-200 text-emerald-900 font-medium"
: "text-gray-700 hover:bg-gray-200"}`}
>
<Home className={`h-4 w-4 mr-2 ${isActive ? "text-emerald-700" : "text-gray-500"}`} />
<span>{subSubItem.lg[lang]}</span>
</Link>
);
})}
</div> </div>
)} )}
</div> </div>

View File

@ -24,7 +24,7 @@ const dashboardLanguage = {
}, },
}; };
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => { const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en", activePage }) => {
const transformedMenu = transformMenu(siteUrls); const transformedMenu = transformMenu(siteUrls);
const t = const t =
dashboardLanguage[lang as keyof typeof dashboardLanguage] || dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
@ -86,7 +86,7 @@ const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
</div> </div>
{/* Navigation Menu */} {/* Navigation Menu */}
<NavigationMenu transformedMenu={transformedMenu} lang={lang} /> <NavigationMenu transformedMenu={transformedMenu} lang={lang} activePage={activePage} />
</div> </div>
); );
}; };

View File

@ -9,7 +9,7 @@ export function retrievePageByUrlAndPageId(
try { try {
const PageComponent = const PageComponent =
PageNavigator[pageUrl as keyof typeof PageNavigator][ PageNavigator[pageUrl as keyof typeof PageNavigator][
pageId as keyof (typeof PageNavigator)[typeof pageUrl] pageId as keyof (typeof PageNavigator)[typeof pageUrl]
]; ];
if (!PageComponent) { if (!PageComponent) {
console.log(`Page component not found for pageId: ${pageId}`); console.log(`Page component not found for pageId: ${pageId}`);

View File

@ -0,0 +1,66 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -1,6 +1,7 @@
interface ClientMenuProps { interface ClientMenuProps {
siteUrls: string[]; siteUrls: string[];
lang?: string; lang?: string;
activePage?: string;
} }
interface UserSelection { interface UserSelection {

View File

@ -0,0 +1,6 @@
const menuPages = {
};
export default menuPages;

View File

@ -0,0 +1,10 @@
import { PageProps } from "../validations/translations/translation";
import { UnAuthorizedPage } from "./unauthorizedpage";
import menuPages from "./index";
export function retrievePageByUrl(url: string): React.FC<PageProps> {
if (url in menuPages) {
return menuPages[url as keyof typeof menuPages];
}
return UnAuthorizedPage;
}

View File

@ -0,0 +1,43 @@
import { PageProps } from "../validations/translations/translation";
// Language dictionary for internationalization
const languageDictionary = {
en: {
title: "Unauthorized Access",
message1: "You do not have permission to access this page.",
message2: "Please contact the administrator.",
footer: `© ${new Date().getFullYear()} My Application`,
},
tr: {
title: "Yetkisiz Erişim",
message1: "Bu sayfaya erişim izniniz yok.",
message2: "Lütfen yönetici ile iletişime geçin.",
footer: `© ${new Date().getFullYear()} Uygulamam`,
},
};
export const UnAuthorizedPage: React.FC<PageProps> = ({
lang = "en",
queryParams,
}) => {
// Use the language dictionary based on the lang prop, defaulting to English
const t =
languageDictionary[lang as keyof typeof languageDictionary] ||
languageDictionary.en;
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">{t.title}</h1>
</header>
<main className="flex-grow p-4 bg-gray-100">
<p className="text-gray-700">{t.message1}</p>
<p className="text-gray-700">{t.message2}</p>
</main>
<footer className="bg-gray-800 text-white p-4 text-center">
<p>{t.footer}</p>
</footer>
</div>
</>
);
};

View File

@ -0,0 +1,30 @@
// Define pagination interface
export interface PagePagination {
page: number;
size: number;
totalCount: number;
allCount: number;
totalPages: number;
orderFields: string[];
orderTypes: string[];
pageCount: number;
query: Record<string, string>;
}
// Define request parameters interface
export interface RequestParams {
page: number;
size: number;
orderFields: string[];
orderTypes: string[];
query: Record<string, string>;
}
// Define response metadata interface
export interface ResponseMetadata {
totalCount: number;
allCount: number;
totalPages: number;
pageCount: number;
}

View File

@ -0,0 +1,11 @@
interface ClientMenuProps {
lang?: string;
activePage: string;
}
interface UserSelection {
userType: string;
selected: any;
}
export type { ClientMenuProps, UserSelection };

View File

@ -0,0 +1,61 @@
// Define TypeScript interfaces for menu structure
interface LanguageTranslation {
tr: string;
en: string;
}
interface MenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
interface MenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: MenuThirdLevel[];
}
interface MenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: MenuSecondLevel[];
}
// Define interfaces for the filtered menu structure
interface FilteredMenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
interface FilteredMenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuThirdLevel[];
}
interface FilteredMenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuSecondLevel[];
}
interface PageProps {
lang: keyof LanguageTranslation;
queryParams: { [key: string]: string | undefined };
}
type PageComponent = React.ComponentType<PageProps>;
export type {
PageComponent,
PageProps,
MenuFirstLevel,
MenuSecondLevel,
MenuThirdLevel,
FilteredMenuFirstLevel,
FilteredMenuSecondLevel,
FilteredMenuThirdLevel,
LanguageTranslation,
};

View File

@ -1,34 +1,22 @@
'use server';
import React from "react"; import React from "react";
import Header from "@/components/header/Header"; import DashboardLayout from "@/components/layouts/DashboardLayout";
import ClientMenu from "@/components/menu/menu"; import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
async function DashboardPage({ async function DashboardPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ [key: string]: string | undefined }>; searchParams: Promise<{ [key: string]: string | undefined }>;
}) { }) {
const activePage = "/application"; const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
const searchParamsInstance = await searchParams; pageUrl: "/application",
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en"; searchParams,
const PageComponent = retrievePageByUrl(activePage); });
return ( return (
<> <DashboardLayout lang={lang} activePage={activePage}>
<div className="min-h-screen min-w-screen flex h-screen w-screen"> <PageComponent lang={lang} queryParams={searchParamsInstance} />
{/* Sidebar */} </DashboardLayout>
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<ClientMenu lang={lang} activePage={activePage} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4 overflow-y-auto">
{/* Header Component */}
<Header lang={lang} />
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>
</>
); );
} }

View File

@ -1,34 +1,23 @@
'use server';
import React from "react"; import React from "react";
import Header from "@/components/header/Header"; import DashboardLayout from "@/components/layouts/DashboardLayout";
import ClientMenu from "@/components/menu/menu"; import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
async function PageDashboard({ async function PageDashboard({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ [key: string]: string | undefined }>; searchParams: Promise<{ [key: string]: string | undefined }>;
}) { }) {
const activePage = "/dashboard";
const searchParamsInstance = await searchParams; const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en"; pageUrl: "/dashboard",
const PageComponent = retrievePageByUrl(activePage); searchParams,
});
return ( return (
<> <DashboardLayout lang={lang} activePage={activePage}>
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden"> <PageComponent lang={lang} queryParams={searchParamsInstance} />
{/* Sidebar */} </DashboardLayout>
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<ClientMenu lang={lang} activePage={activePage} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4">
{/* Header Component */}
<Header lang={lang} />
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>
</>
); );
} }

View File

@ -0,0 +1,108 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
// Translations for error messages
const translations = {
en: {
errorTitle: 'An error occurred',
errorMessage: 'Sorry, something went wrong while loading this page.',
unknownError: 'Unknown error',
errorId: 'Error ID',
tryAgain: 'Try again',
goHome: 'Go to Home'
},
tr: {
errorTitle: 'Bir hata oluştu',
errorMessage: 'Üzgünüz, bu sayfa yüklenirken bir sorun oluştu.',
unknownError: 'Bilinmeyen hata',
errorId: 'Hata ID',
tryAgain: 'Tekrar dene',
goHome: 'Ana Sayfaya Git'
}
};
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}
/**
* Global error page for the Dashboard Layout
* This component will be automatically used by Next.js when an error occurs
* in any page within the (DashboardLayout) group
*/
export default function Error({ error, reset }: ErrorPageProps) {
// Get the language from URL params or default to English
const searchParams = useSearchParams();
const [lang, setLang] = useState<'en' | 'tr'>((searchParams?.get('lang') as 'en' | 'tr') || 'en');
// Ensure lang is valid
const validLang = lang === 'tr' ? 'tr' : 'en';
const t = translations[validLang];
useEffect(() => {
// Log the error to an error reporting service
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<Card className="w-full max-w-md shadow-lg">
<CardHeader className="bg-red-50">
<div className="flex items-center space-x-2">
<AlertTriangle className="h-6 w-6 text-red-600" />
<CardTitle className="text-red-700">{t.errorTitle}</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-6">
<div className="space-y-4">
<p className="text-gray-700">
{t.errorMessage}
</p>
<div className="p-4 bg-gray-100 rounded-md">
<p className="font-mono text-sm break-all">
{error.message || t.unknownError}
</p>
{error.digest && (
<p className="font-mono text-xs text-gray-500 mt-2">
{t.errorId}: {error.digest}
</p>
)}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between space-x-4 pt-2">
<Button
variant="outline"
className="flex-1"
onClick={() => reset()}
>
<RefreshCw className="mr-2 h-4 w-4" />
{t.tryAgain}
</Button>
<Button
variant="default"
className="flex-1 bg-primary"
asChild
>
<Link href="/">
<Home className="mr-2 h-4 w-4" />
{t.goHome}
</Link>
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@ -0,0 +1,108 @@
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
// Translations for error messages
const translations = {
en: {
errorTitle: 'An error occurred',
errorMessage: 'Sorry, something went wrong while loading this page.',
unknownError: 'Unknown error',
errorId: 'Error ID',
tryAgain: 'Try again',
goHome: 'Go to Home'
},
tr: {
errorTitle: 'Bir hata oluştu',
errorMessage: 'Üzgünüz, bu sayfa yüklenirken bir sorun oluştu.',
unknownError: 'Bilinmeyen hata',
errorId: 'Hata ID',
tryAgain: 'Tekrar dene',
goHome: 'Ana Sayfaya Git'
}
};
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}
/**
* Global error page for the Dashboard Layout
* This component will be automatically used by Next.js when an error occurs
* in any page within the (DashboardLayout) group
*/
export default function Error({ error, reset }: ErrorPageProps) {
// Get the language from URL params or default to English
const searchParams = useSearchParams();
const [lang, setLang] = useState<'en' | 'tr'>((searchParams?.get('lang') as 'en' | 'tr') || 'en');
// Ensure lang is valid
const validLang = lang === 'tr' ? 'tr' : 'en';
const t = translations[validLang];
useEffect(() => {
// Log the error to an error reporting service
console.error('Dashboard error:', error);
}, [error]);
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<Card className="w-full max-w-md shadow-lg">
<CardHeader className="bg-red-50">
<div className="flex items-center space-x-2">
<AlertTriangle className="h-6 w-6 text-red-600" />
<CardTitle className="text-red-700">{t.errorTitle}</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-6">
<div className="space-y-4">
<p className="text-gray-700">
{t.errorMessage}
</p>
<div className="p-4 bg-gray-100 rounded-md">
<p className="font-mono text-sm break-all">
{error.message || t.unknownError}
</p>
{error.digest && (
<p className="font-mono text-xs text-gray-500 mt-2">
{t.errorId}: {error.digest}
</p>
)}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between space-x-4 pt-2">
<Button
variant="outline"
className="flex-1"
onClick={() => reset()}
>
<RefreshCw className="mr-2 h-4 w-4" />
{t.tryAgain}
</Button>
<Button
variant="default"
className="flex-1 bg-primary"
asChild
>
<Link href="/">
<Home className="mr-2 h-4 w-4" />
{t.goHome}
</Link>
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { CreateComponentProps } from "./types"; import { CreateComponentProps, FieldDefinition } from "./types";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
// Import field definitions type
interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string;
}
export function CreateComponent<T>({ export function CreateComponent<T>({
refetch, refetch,
setMode, setMode,
@ -140,13 +128,13 @@ export function CreateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldName} id={fieldName}
{...register(fieldName)} {...register(fieldName)}
placeholder={t[fieldName] || field.label} placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
disabled={field.readOnly} disabled={field.readOnly}
/> />
{errorMessage && ( {errorMessage && (
@ -159,13 +147,13 @@ export function CreateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Textarea <Textarea
id={fieldName} id={fieldName}
{...register(fieldName)} {...register(fieldName)}
placeholder={t[fieldName] || field.label} placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
rows={3} rows={3}
disabled={field.readOnly} disabled={field.readOnly}
/> />
@ -179,7 +167,7 @@ export function CreateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Select <Select
@ -188,7 +176,7 @@ export function CreateComponent<T>({
disabled={field.readOnly} disabled={field.readOnly}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={t[fieldName] || field.label} /> <SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{field.options?.map((option) => ( {field.options?.map((option) => (
@ -216,7 +204,7 @@ export function CreateComponent<T>({
disabled={field.readOnly} disabled={field.readOnly}
/> />
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
{errorMessage && ( {errorMessage && (

View File

@ -69,13 +69,14 @@ export function FormDisplay<T>({
}; };
loadSchemaDefinitions(); loadSchemaDefinitions();
}, [formProps, mode]); }, [formProps, mode, lang]); // Added lang as a dependency to ensure re-fetch when language changes
// Render the appropriate component based on the mode // Render the appropriate component based on the mode
switch (mode) { switch (mode) {
case "create": case "create":
return ( return (
<CreateComponent<T> <CreateComponent<T>
key={`create-${lang}`} // Add key with lang to force re-render on language change
refetch={refetch} refetch={refetch}
setMode={setMode} setMode={setMode}
setSelectedItem={setSelectedItem} setSelectedItem={setSelectedItem}
@ -88,6 +89,7 @@ export function FormDisplay<T>({
case "update": case "update":
return initialData ? ( return initialData ? (
<UpdateComponent<T> <UpdateComponent<T>
key={`update-${lang}`} // Add key with lang to force re-render on language change
initialData={initialData} initialData={initialData}
refetch={refetch} refetch={refetch}
setMode={setMode} setMode={setMode}
@ -101,6 +103,7 @@ export function FormDisplay<T>({
case "view": case "view":
return initialData ? ( return initialData ? (
<ViewComponent<T> <ViewComponent<T>
key={`view-${lang}`} // Add key with lang to force re-render on language change
initialData={initialData} initialData={initialData}
refetch={refetch} refetch={refetch}
setMode={setMode} setMode={setMode}
@ -108,7 +111,7 @@ export function FormDisplay<T>({
onCancel={onCancel} onCancel={onCancel}
lang={lang} lang={lang}
translations={translations} translations={translations}
formProps={formProps} formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
/> />
) : null; ) : null;
default: default:

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { UpdateComponentProps } from "./types"; import { UpdateComponentProps, FieldDefinition } from "./types";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
// Import field definitions type
interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string; // Add name property for TypeScript compatibility
}
export function UpdateComponent<T>({ export function UpdateComponent<T>({
initialData, initialData,
refetch, refetch,
@ -154,13 +142,13 @@ export function UpdateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input
id={fieldName} id={fieldName}
{...register(fieldName)} {...register(fieldName)}
placeholder={t[fieldName] || field.label} placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
disabled={field.readOnly} disabled={field.readOnly}
/> />
{errorMessage && ( {errorMessage && (
@ -173,13 +161,13 @@ export function UpdateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Textarea <Textarea
id={fieldName} id={fieldName}
{...register(fieldName)} {...register(fieldName)}
placeholder={t[fieldName] || field.label} placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
rows={3} rows={3}
disabled={field.readOnly} disabled={field.readOnly}
/> />
@ -193,7 +181,7 @@ export function UpdateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Select <Select
@ -202,7 +190,7 @@ export function UpdateComponent<T>({
disabled={field.readOnly} disabled={field.readOnly}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={t[fieldName] || field.label} /> <SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{field.options?.map((option) => ( {field.options?.map((option) => (
@ -230,7 +218,7 @@ export function UpdateComponent<T>({
disabled={field.readOnly} disabled={field.readOnly}
/> />
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
{errorMessage && ( {errorMessage && (
@ -243,7 +231,7 @@ export function UpdateComponent<T>({
return ( return (
<div className="space-y-2" key={fieldName}> <div className="space-y-2" key={fieldName}>
<Label htmlFor={fieldName}> <Label htmlFor={fieldName}>
{t[fieldName] || field.label} {t[fieldName] || field.label[lang as "en" | "tr"]}
{field.required && <span className="text-red-500 ml-1">*</span>} {field.required && <span className="text-red-500 ml-1">*</span>}
</Label> </Label>
<Input <Input

View File

@ -2,21 +2,10 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ViewComponentProps } from "./types"; import { ViewComponentProps, FieldDefinition } from "./types";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { z } from "zod"; import { z } from "zod";
// Import field definitions type
export interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string; // Add name property for TypeScript compatibility
}
// Utility function to format field label // Utility function to format field label
const formatFieldLabel = (fieldName: string) => const formatFieldLabel = (fieldName: string) =>
@ -103,7 +92,7 @@ const ViewFieldGroup: React.FC<{
key={fieldName} key={fieldName}
fieldName={fieldName} fieldName={fieldName}
value={value} value={value}
label={field.label} label={field.label[lang as "en" | "tr"]}
lang={lang} lang={lang}
translations={translations} translations={translations}
hasError={hasError} hasError={hasError}

View File

@ -1,5 +1,17 @@
"use client"; "use client";
// Import field definitions type
export interface FieldDefinition {
type: string;
group: string;
label: { tr: string; en: string };
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
name?: string;
}
// Define the FormMode type to ensure consistency // Define the FormMode type to ensure consistency
export type FormMode = "list" | "create" | "update" | "view"; export type FormMode = "list" | "create" | "update" | "view";

View File

@ -1,4 +1,5 @@
import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching"; import { useDataFetching, ApiResponse } from "./useDataFetching";
import { RequestParams } from "../schemas";
/** /**
* Hook for fetching data from Next.js API routes * Hook for fetching data from Next.js API routes
@ -11,35 +12,37 @@ export function useApiData<T>(
initialParams: Partial<RequestParams> = {} initialParams: Partial<RequestParams> = {}
) { ) {
// Define the fetch function that will be passed to useDataFetching // Define the fetch function that will be passed to useDataFetching
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => { const fetchFromApi = async (
params: RequestParams
): Promise<ApiResponse<T>> => {
try { try {
// Prepare the request body with action and all params // Prepare the request body with action and all params
const requestBody = { const requestBody = {
action: 'list', action: "list",
page: params.page, page: params.page,
size: params.size, size: params.size,
orderField: params.orderField, orderField: params.orderField,
orderType: params.orderType, orderType: params.orderType,
query: params.query query: params.query,
}; };
// Make the API request using POST // Make the API request using POST
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`); throw new Error(`API request failed with status ${response.status}`);
} }
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching data from API:", error); console.error("Error fetching data from API:", error);
// Return empty data with pagination info on error // Return empty data with pagination info on error
return { return {
data: [], data: [],

View File

@ -0,0 +1,93 @@
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
import { PageProps } from "@/validations/translations/translation";
import React, { ReactElement } 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>;
}
/**
* Hook to retrieve and prepare dashboard page data
* Throws errors for Next.js error boundary to catch
*
* @param params The dashboard page parameters
* @returns The processed dashboard page data
* @throws Error if page URL is invalid or page component is not found
*/
export async function useDashboardPage({
pageUrl,
searchParams,
}: 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
if (!PageComponent) {
throw new Error(`Page component not found for URL: ${pageUrl}`);
}
return {
activePage: pageUrl,
searchParamsInstance,
lang,
PageComponent,
};
}
export default useDashboardPage;

View File

@ -1,22 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { RequestParams, ResponseMetadata } from "../schemas";
export interface RequestParams {
page: number;
size: number;
orderField: string[];
orderType: string[];
query: Record<string, any>;
}
export interface ResponseMetadata {
totalCount: number;
totalItems: number;
totalPages: number;
pageCount: number;
allCount?: number;
next: boolean;
back: boolean;
}
export interface PagePagination extends RequestParams, ResponseMetadata {} export interface PagePagination extends RequestParams, ResponseMetadata {}
@ -104,19 +87,21 @@ export function useDataFetching<T>(
// Track if this is the initial mount // Track if this is the initial mount
const initialMountRef = useRef(true); const initialMountRef = useRef(true);
// Track previous request params to avoid unnecessary fetches // Track previous request params to avoid unnecessary fetches
const prevRequestParamsRef = useRef<RequestParams>(requestParams); const prevRequestParamsRef = useRef<RequestParams>(requestParams);
useEffect(() => { useEffect(() => {
// Only fetch on mount or when request params actually change // Only fetch on mount or when request params actually change
const paramsChanged = JSON.stringify(prevRequestParamsRef.current) !== JSON.stringify(requestParams); const paramsChanged =
JSON.stringify(prevRequestParamsRef.current) !==
JSON.stringify(requestParams);
if (initialMountRef.current || paramsChanged) { if (initialMountRef.current || paramsChanged) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
fetchDataFromApi(); fetchDataFromApi();
initialMountRef.current = false; initialMountRef.current = false;
prevRequestParamsRef.current = {...requestParams}; prevRequestParamsRef.current = { ...requestParams };
}, 300); // Debounce }, 300); // Debounce
return () => clearTimeout(timer); return () => clearTimeout(timer);

View File

@ -0,0 +1,181 @@
import { useState, useEffect } from 'react';
/**
* Cache options for the fetch request
*/
export type CacheOptions = {
/** Whether to cache the request (default: true) */
cache?: boolean;
/** Revalidate time in seconds (if not provided, uses Next.js defaults) */
revalidate?: number;
/** Force cache to be revalidated (equivalent to cache: 'no-store' in fetch) */
noStore?: boolean;
};
/**
* Request options for the fetch
*/
export type FetchOptions = {
/** HTTP method (default: 'GET') */
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
/** Request headers */
headers?: HeadersInit;
/** Request body (for POST, PUT, PATCH) */
body?: any;
};
/**
* A hook for fetching data from an API endpoint without pagination using Next.js fetch
* @param url The API endpoint URL
* @param initialParams Initial query parameters
* @param options Additional fetch options
* @param cacheOptions Cache control options
* @returns Object containing data, loading state, error state, and refetch function
*/
export function useStandardApiFetch<T>(
url: string,
initialParams: Record<string, any> = {},
options: FetchOptions = {},
cacheOptions: CacheOptions = { cache: true }
) {
const [data, setData] = useState<T | null>(null);
const [params, setParams] = useState<Record<string, any>>(initialParams);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
/**
* Builds the URL with query parameters
*/
const buildUrl = () => {
const queryParams = new URLSearchParams();
// Add all non-null and non-empty params
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== '') {
queryParams.append(key, String(value));
}
});
const queryString = queryParams.toString();
return queryString ? `${url}?${queryString}` : url;
};
/**
* Configure fetch options including cache settings
*/
const getFetchOptions = (): RequestInit => {
const { method = 'GET', headers = {}, body } = options;
const fetchOptions: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
};
// Add body for non-GET requests if provided
if (method !== 'GET' && body) {
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
}
// Configure cache options
if (!cacheOptions.cache) {
fetchOptions.cache = 'no-store';
} else if (cacheOptions.noStore) {
fetchOptions.cache = 'no-store';
} else if (cacheOptions.revalidate !== undefined) {
fetchOptions.next = { revalidate: cacheOptions.revalidate };
}
return fetchOptions;
};
const fetchData = async () => {
setLoading(true);
try {
const fullUrl = buildUrl();
const fetchOptions = getFetchOptions();
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const responseData = await response.json();
setData(responseData);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An unknown error occurred'));
setData(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url, JSON.stringify(params), JSON.stringify(options), JSON.stringify(cacheOptions)]);
/**
* Update the query parameters and trigger a refetch
* @param newParams New parameters to merge with existing ones
*/
const updateParams = (newParams: Record<string, any>) => {
// Filter out null or empty string values
const filteredParams = Object.entries(newParams).reduce((acc, [key, value]) => {
if (value !== null && value !== '') {
acc[key] = value;
}
return acc;
}, {} as Record<string, any>);
setParams(prev => ({
...prev,
...filteredParams
}));
};
/**
* Reset all parameters to initial values
*/
const resetParams = () => {
setParams(initialParams);
};
/**
* Manually trigger a refetch of the data
*/
const refetch = () => {
fetchData();
};
return {
data,
loading,
error,
updateParams,
resetParams,
refetch
};
}
// // Basic usage (with default caching)
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>('/api/your-endpoint');
// // With no caching (for data that changes frequently)
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
// '/api/your-endpoint',
// {},
// {},
// { cache: false }
// );
// // With specific revalidation time
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
// '/api/your-endpoint',
// {},
// {},
// { revalidate: 60 } // Revalidate every 60 seconds
// );

View File

@ -0,0 +1,72 @@
"use client";
import React, { ReactNode } from "react";
import Header from "@/components/header/Header";
import ClientMenu from "@/components/menu/menu";
interface DashboardLayoutProps {
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>;
}
/**
* A reusable dashboard layout component that provides consistent structure
* for all dashboard pages with sidebar, header, and content area.
*/
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
lang,
activePage,
sidebarContent,
customHeader,
pageInfo,
searchPlaceholder,
}) => {
return (
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-y-auto">
{/* Sidebar */}
<aside className="w-1/4 border-r p-4 overflow-y-auto">
{sidebarContent ? (
sidebarContent
) : (
<ClientMenu lang={lang} 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} />
)}
{/* Page Content */}
<div className={`${customHeader ? 'p-4 overflow-y-auto' : 'container mx-auto p-4'}`}>
{children}
</div>
</div>
</div>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,155 @@
"use client";
import React, { ReactNode, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
import type { FormMode } from "@/components/common/FormDisplay/types";
interface PageTemplateProps {
title: string;
lang: "en" | "tr";
translations: Record<string, any>;
// Search section
searchSection?: ReactNode;
// Data and pagination
data: any[];
pagination: any;
updatePagination: (params: any) => void;
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;
}
/**
* A reusable page template that follows the modular pattern established
* in the card-example page refactoring.
*/
export const PageTemplate: React.FC<PageTemplateProps> = ({
title,
lang,
translations,
searchSection,
data,
pagination,
updatePagination,
loading,
error,
refetch,
contentDisplay,
formComponent,
mode,
setMode,
handleCreateClick,
handleCancel,
setLang,
headerActions,
additionalActions,
}) => {
const [gridCols, setGridCols] = useState<GridSize>(3);
return (
<>
{mode === "list" ? (
<>
{/* Header section with title and selection components */}
<div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">{title}</h1>
<div className="flex space-x-4">
{/* Grid Selection */}
<GridSelectionComponent
gridCols={gridCols}
setGridCols={setGridCols}
/>
{/* Language Selection */}
<LanguageSelectionComponent
lang={lang as Language}
translations={translations}
setLang={setLang || (() => {})}
/>
{/* Additional header actions */}
{headerActions}
</div>
</div>
{/* Search filters */}
{searchSection && (
<Card className="mb-4">
<CardContent className="pt-6">
{searchSection}
</CardContent>
</Card>
)}
{/* Create Button */}
<Card className="my-4">
<CardContent className="pt-6">
<CreateButton
onClick={handleCreateClick}
translations={translations}
lang={lang}
/>
{/* Additional action buttons */}
{additionalActions}
</CardContent>
</Card>
{/* Pagination Tools Component */}
<Card className="my-4">
<CardContent className="pt-6">
<PaginationToolsComponent
pagination={pagination}
updatePagination={updatePagination}
loading={loading}
lang={lang}
translations={translations}
/>
</CardContent>
</Card>
{/* Content Display */}
<div className="mt-6">
{contentDisplay}
</div>
</>
) : (
/* Form Display for create/update/view modes */
formComponent || (
<div className="p-4 bg-gray-100 rounded-md">
<p>Form component not provided</p>
<button
onClick={handleCancel}
className="mt-4 px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300"
>
{translations[lang].cancel || "Cancel"}
</button>
</div>
)
)}
</>
);
};
export default PageTemplate;

View File

@ -0,0 +1,271 @@
# Modular Dashboard Layout System
This directory contains reusable layout components for building dashboard pages with a consistent structure and appearance.
## Components
### DashboardLayout
The `DashboardLayout` component provides the overall page structure with:
- Sidebar navigation
- Header
- Main content area
### PageTemplate
The `PageTemplate` component provides a standardized structure for page content with:
- Header section with title and selection components
- Search filters section
- Action buttons section
- Pagination tools section
- Content display section
- Form display for create/update/view modes
## Usage Example
Here's an example of how to use these components to create a new dashboard page:
```tsx
"use client";
import React, { useState } from "react";
import { useApiData } from "@/components/common/hooks/useApiData";
import { CardDisplay } from "@/components/common/CardDisplay";
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
import { Card, CardContent } from "@/components/ui/card";
import { Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import { FormMode } from "@/components/common/FormDisplay/types";
import { PageTemplate } from "@/components/layouts/PageTemplate";
import type { GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
// Import your schema and translations
import * as schema from "./schema";
import { translations } from "./language";
const ExamplePage: React.FC<{ lang: "en" | "tr", queryParams: any }> = ({
lang = "en",
queryParams
}) => {
// API data hook
const {
data,
pagination,
loading,
error,
updatePagination,
refetch
} = useApiData<schema.ExampleData>('/api/examples');
// State management
const [mode, setMode] = useState<FormMode>("list");
const [selectedItem, setSelectedItem] = useState<schema.ExampleData | null>(null);
const [gridCols, setGridCols] = useState<GridSize>(3);
const [currentLang, setCurrentLang] = useState<Language>(lang as Language);
// Fields to display in cards
const showFields = ["name", "type", "status"];
// Query handling
const handleQueryChange = (key: string, value: string | null) => {
const newQuery = { ...pagination.query };
if (value === null || value.trim() === "") {
delete newQuery[key];
} else {
newQuery[key] = value;
}
updatePagination({
page: 1,
query: newQuery,
});
};
// Reset all filters
const handleResetAllFilters = () => {
updatePagination({
page: 1,
query: {},
});
};
// Action handlers
const handleCardClick = (item: schema.ExampleData) => {
console.log("Card clicked:", item);
};
const handleViewClick = (item: schema.ExampleData) => {
setSelectedItem(item);
setMode("view");
};
const handleUpdateClick = (item: schema.ExampleData) => {
setSelectedItem(item);
setMode("update");
};
const handleCreateClick = () => {
setSelectedItem(null);
setMode("create");
};
const handleCancel = () => {
setMode("list");
setSelectedItem(null);
};
// Search section component
const SearchSection = (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
{/* Type selector */}
<TypeQueryModifier
fieldKey="type"
value={pagination.query["type"] || ""}
options={[
{ value: "type1", label: translations[lang].type1 },
{ value: "type2", label: translations[lang].type2 }
]}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
</div>
<div className="flex space-x-4">
{/* Text search */}
<TextQueryModifier
fieldKey="name"
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
label={translations[lang].search || "Search"}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
{/* Status dropdown */}
<SelectQueryModifier
fieldKey="status"
value={pagination.query["status"] || ""}
label={translations[lang].status || "Status"}
options={[
{ value: "active", label: translations[lang].active || "Active" },
{ value: "inactive", label: translations[lang].inactive || "Inactive" }
]}
onQueryChange={handleQueryChange}
translations={translations}
lang={lang}
/>
</div>
{/* Reset filters button */}
<div className="flex justify-end">
<button
onClick={handleResetAllFilters}
className="px-4 py-2 bg-gray-100 rounded-md hover:bg-gray-200"
>
{translations[lang].resetAll || "Reset All"}
</button>
</div>
</div>
);
// Form component
const FormComponent = selectedItem || mode === "create" ? (
<FormDisplay<schema.ExampleData>
initialData={selectedItem || undefined}
mode={mode}
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={handleCancel}
lang={lang}
translations={translations}
formProps={{
fieldDefinitions: mode === 'create' ? schema.createFieldDefinitions :
mode === 'update' ? schema.updateFieldDefinitions :
schema.viewFieldDefinitions,
validationSchema: mode === 'create' ? schema.CreateExampleSchema :
mode === 'update' ? schema.UpdateExampleSchema :
schema.ViewExampleSchema,
fieldsByMode: schema.fieldsByMode
}}
/>
) : null;
// Content display component
const ContentDisplay = (
<CardDisplay
showFields={showFields}
data={data}
lang={lang}
translations={translations}
error={error}
loading={loading}
titleField="name"
onCardClick={handleCardClick}
gridCols={gridCols}
showViewIcon={true}
showUpdateIcon={true}
onViewClick={handleViewClick}
onUpdateClick={handleUpdateClick}
/>
);
return (
<PageTemplate
title={translations[lang].examplePageTitle || "Example Page"}
lang={lang}
translations={translations}
searchSection={SearchSection}
data={data}
pagination={pagination}
updatePagination={updatePagination}
loading={loading}
error={error}
refetch={refetch}
contentDisplay={ContentDisplay}
formComponent={FormComponent}
mode={mode}
setMode={setMode}
handleCreateClick={handleCreateClick}
handleCancel={handleCancel}
setLang={setCurrentLang}
/>
);
};
export default ExamplePage;
```
## Integration with Next.js App Router
To use these components with Next.js App Router, update your page component like this:
```tsx
// src/app/(DashboardLayout)/your-page/page.tsx
import React from "react";
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
import DashboardLayout from "@/components/layouts/DashboardLayout";
async function YourPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | undefined }>;
}) {
const activePage = "/your-page";
const searchParamsInstance = await searchParams;
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
const PageComponent = retrievePageByUrl(activePage);
return (
<DashboardLayout lang={lang} activePage={activePage}>
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</DashboardLayout>
);
}
export default YourPage;
```
This modular approach makes it easy to create new dashboard pages with consistent structure and behavior.

View File

@ -16,7 +16,10 @@ import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSele
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent"; import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import type { FormMode } from "@/components/common/FormDisplay/types"; import type { FormMode } from "@/components/common/FormDisplay/types";
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => { 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);
// Use the API data hook directly // Use the API data hook directly
const { const {
data, data,
@ -128,23 +131,22 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
}; };
return ( return (
<div className="container mx-auto p-4"> <div className="container mx-auto p-4 overflow-y-auto" >
<div className="mb-4 flex justify-between items-center"> <div className="mb-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1> <h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1>
<div className="flex items-center space-x-4"> <div className="flex space-x-4">
<GridSelectionComponent {/* Grid Selection */}
gridCols={gridCols} <GridSelectionComponent
setGridCols={setGridCols} gridCols={gridCols}
translations={translations} setGridCols={setGridCols}
lang={lang} />
{/* Language Selection */}
<LanguageSelectionComponent
lang={lang}
translations={translations}
setLang={setLang}
/> />
<div>
<LanguageSelectionComponent
lang={lang as Language}
setLang={(newLang) => console.log("Language change not implemented", newLang)}
translations={translations}
/>
</div>
</div> </div>
</div> </div>
@ -174,14 +176,14 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
<Filter className="mr-2 h-4 w-4" /> <Filter className="mr-2 h-4 w-4" />
{translations[lang].filterSelection || "Filter Selection"} {translations[lang].filterSelection || "Filter Selection"}
</div> </div>
<Button {/* <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleResetAllFilters} onClick={handleResetAllFilters}
className="text-xs" className="text-xs"
> >
{translations[lang].resetAll || "Reset All"} {translations[lang].resetAll || "Reset All"}
</Button> </Button> */}
</div> </div>
{/* Search input */} {/* Search input */}

View File

@ -1,4 +1,19 @@
import { z } from "zod"; import { z } from "zod";
import { flattenFieldDefinitions } from "../schemas/zodSchemas";
export interface ApplicationData {
id?: number;
name: string;
application_code: string;
site_url: string;
application_type: string;
application_for?: string;
description?: string;
active: boolean;
deleted?: boolean;
created_at?: string;
updated_at?: string;
}
// Base schema with all possible fields // Base schema with all possible fields
const ApplicationBaseSchema = z.object({ const ApplicationBaseSchema = z.object({
@ -22,6 +37,34 @@ const ApplicationBaseSchema = z.object({
updated_at: z.string().optional(), updated_at: z.string().optional(),
}); });
const ApplicationBaseTranslationTr = {
uu_id: "UUID",
name: "Name",
application_code: "Application Code",
site_url: "Site URL",
application_type: "Application Type",
application_for: "Application For",
description: "Description",
active: "Active",
deleted: "Deleted",
created_at: "Created At",
updated_at: "Updated At",
};
const ApplicationBaseTranslationEn = {
uu_id: "UUID",
name: "Name",
application_code: "Application Code",
site_url: "Site URL",
application_type: "Application Type",
application_for: "Application For",
description: "Description",
active: "Active",
deleted: "Deleted",
created_at: "Created At",
updated_at: "Updated At",
};
// Schema for creating a new application // Schema for creating a new application
export const CreateApplicationSchema = ApplicationBaseSchema.omit({ export const CreateApplicationSchema = ApplicationBaseSchema.omit({
uu_id: true, uu_id: true,
@ -50,31 +93,6 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>; export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>; export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
// Define field definition type
export interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
}
export interface ApplicationData {
id?: number;
name: string;
application_code: string;
site_url: string;
application_type: string;
application_for?: string;
description?: string;
active: boolean;
deleted?: boolean;
created_at?: string;
updated_at?: string;
}
// Base field definitions grouped by section // Base field definitions grouped by section
const baseFieldDefinitions = { const baseFieldDefinitions = {
// Identification fields // Identification fields
@ -82,11 +100,30 @@ const baseFieldDefinitions = {
title: "Identification Information", title: "Identification Information",
order: 1, order: 1,
fields: { fields: {
uu_id: { type: "text", label: "UUID", readOnly: true, required: false }, uu_id: {
name: { type: "text", label: "Name", readOnly: false, required: true }, type: "text",
label: {
tr: ApplicationBaseTranslationTr.uu_id,
en: ApplicationBaseTranslationEn.uu_id,
},
readOnly: true,
required: false,
},
name: {
type: "text",
label: {
tr: ApplicationBaseTranslationTr.name,
en: ApplicationBaseTranslationEn.name,
},
readOnly: false,
required: true,
},
application_code: { application_code: {
type: "text", type: "text",
label: "Application Code", label: {
tr: ApplicationBaseTranslationTr.application_code,
en: ApplicationBaseTranslationEn.application_code,
},
readOnly: false, readOnly: false,
required: true, required: true,
}, },
@ -100,27 +137,39 @@ const baseFieldDefinitions = {
fields: { fields: {
site_url: { site_url: {
type: "text", type: "text",
label: "Site URL", label: {
tr: ApplicationBaseTranslationTr.site_url,
en: ApplicationBaseTranslationEn.site_url,
},
readOnly: false, readOnly: false,
required: true, required: true,
}, },
application_type: { application_type: {
type: "select", type: "select",
label: "Application Type", label: {
tr: ApplicationBaseTranslationTr.application_type,
en: ApplicationBaseTranslationEn.application_type,
},
options: ["info", "Dash", "Admin"], options: ["info", "Dash", "Admin"],
readOnly: false, readOnly: false,
required: true, required: true,
}, },
application_for: { application_for: {
type: "select", type: "select",
label: "Application For", label: {
tr: ApplicationBaseTranslationTr.application_for,
en: ApplicationBaseTranslationEn.application_for,
},
options: ["EMP", "OCC"], options: ["EMP", "OCC"],
readOnly: false, readOnly: false,
required: false, required: false,
}, },
description: { description: {
type: "textarea", type: "textarea",
label: "Description", label: {
tr: ApplicationBaseTranslationTr.description,
en: ApplicationBaseTranslationEn.description,
},
readOnly: false, readOnly: false,
required: false, required: false,
}, },
@ -134,14 +183,20 @@ const baseFieldDefinitions = {
fields: { fields: {
active: { active: {
type: "checkbox", type: "checkbox",
label: "Active", label: {
tr: ApplicationBaseTranslationTr.active,
en: ApplicationBaseTranslationEn.active,
},
readOnly: false, readOnly: false,
required: false, required: false,
defaultValue: true, defaultValue: true,
}, },
deleted: { deleted: {
type: "checkbox", type: "checkbox",
label: "Deleted", label: {
tr: ApplicationBaseTranslationTr.deleted,
en: ApplicationBaseTranslationEn.deleted,
},
readOnly: true, readOnly: true,
required: false, required: false,
defaultValue: false, defaultValue: false,
@ -156,13 +211,19 @@ const baseFieldDefinitions = {
fields: { fields: {
created_at: { created_at: {
type: "date", type: "date",
label: "Created At", label: {
tr: ApplicationBaseTranslationTr.created_at,
en: ApplicationBaseTranslationEn.created_at,
},
readOnly: true, readOnly: true,
required: false, required: false,
}, },
updated_at: { updated_at: {
type: "date", type: "date",
label: "Updated At", label: {
tr: ApplicationBaseTranslationTr.updated_at,
en: ApplicationBaseTranslationEn.updated_at,
},
readOnly: true, readOnly: true,
required: false, required: false,
}, },
@ -170,28 +231,6 @@ const baseFieldDefinitions = {
}, },
}; };
// Helper function to flatten grouped field definitions into a flat structure
const flattenFieldDefinitions = (
groupedDefs: any
): Record<string, FieldDefinition> => {
const result: Record<string, FieldDefinition> = {};
Object.entries(groupedDefs).forEach(
([groupName, groupConfig]: [string, any]) => {
Object.entries(groupConfig.fields).forEach(
([fieldName, fieldConfig]: [string, any]) => {
result[fieldName] = {
...fieldConfig,
group: groupName,
};
}
);
}
);
return result;
};
// Create a flat version of the field definitions for compatibility // Create a flat version of the field definitions for compatibility
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions); const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
@ -386,6 +425,3 @@ export const fieldsByMode = {
update: Object.keys(updateFieldDefinitions), update: Object.keys(updateFieldDefinitions),
view: Object.keys(viewFieldDefinitions), view: Object.keys(viewFieldDefinitions),
}; };
// Note: Direct fetch function has been removed to use the API route instead
// Data fetching is now handled in the hooks.ts file using the POST endpoint

View File

@ -0,0 +1,35 @@
// Define field definition type
interface FieldDefinition {
type: string;
group: string;
label: string;
options?: string[];
readOnly?: boolean;
required?: boolean;
defaultValue?: any;
}
// Helper function to flatten grouped field definitions into a flat structure
const flattenFieldDefinitions = (
groupedDefs: any
): Record<string, FieldDefinition> => {
const result: Record<string, FieldDefinition> = {};
Object.entries(groupedDefs).forEach(
([groupName, groupConfig]: [string, any]) => {
Object.entries(groupConfig.fields).forEach(
([fieldName, fieldConfig]: [string, any]) => {
result[fieldName] = {
...fieldConfig,
group: groupName,
};
}
);
}
);
return result;
};
export type { FieldDefinition };
export { flattenFieldDefinitions };

View File

@ -1,17 +1,17 @@
services: services:
# client_frontend: client_frontend:
# container_name: client_frontend container_name: client_frontend
# build: build:
# context: . context: .
# dockerfile: WebServices/client-frontend/Dockerfile dockerfile: WebServices/client-frontend/Dockerfile
# networks: networks:
# - wag-services - wag-services
# ports: ports:
# - "3000:3000" - "3000:3000"
# environment: environment:
# - NODE_ENV=development - NODE_ENV=development
# cpus: 1 cpus: 1
# mem_limit: 2048m mem_limit: 2048m
# volumes: # volumes:
# - client-frontend:/WebServices/client-frontend # - client-frontend:/WebServices/client-frontend
@ -57,32 +57,32 @@ services:
mem_limit: 512m mem_limit: 512m
cpus: 0.5 cpus: 0.5
# identity_service: identity_service:
# container_name: identity_service container_name: identity_service
# build: build:
# context: . context: .
# dockerfile: ApiServices/IdentityService/Dockerfile dockerfile: ApiServices/IdentityService/Dockerfile
# networks: networks:
# - wag-services - wag-services
# depends_on: depends_on:
# - initializer_service - initializer_service
# env_file: env_file:
# - api_env.env - api_env.env
# environment: environment:
# - API_PATH=app:app - API_PATH=app:app
# - API_HOST=0.0.0.0 - API_HOST=0.0.0.0
# - API_PORT=8002 - API_PORT=8002
# - API_LOG_LEVEL=info - API_LOG_LEVEL=info
# - API_RELOAD=1 - API_RELOAD=1
# - API_APP_NAME=evyos-identity-api-gateway - API_APP_NAME=evyos-identity-api-gateway
# - API_TITLE=WAG API Identity Api Gateway - API_TITLE=WAG API Identity Api Gateway
# - API_FORGOT_LINK=https://identity_service/forgot-password - API_FORGOT_LINK=https://identity_service/forgot-password
# - API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services. - API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
# - API_APP_URL=https://identity_service - API_APP_URL=https://identity_service
# ports: ports:
# - "8002:8002" - "8002:8002"
# mem_limit: 512m mem_limit: 512m
# cpus: 0.5 cpus: 0.5
# building_service: # building_service:
# container_name: building_service # container_name: building_service