updated lang change and FormDisplay Components
This commit is contained in:
parent
f2cc7a69b5
commit
36e63960f8
|
|
@ -34,6 +34,7 @@ npx shadcn@latest add calendar -y
|
|||
npx shadcn@latest add date-picker -y
|
||||
npx shadcn@latest add skeleton -y
|
||||
npx shadcn@latest add table -y
|
||||
npx shadcn@latest add textarea -y
|
||||
|
||||
# Update any dependencies with legacy peer deps
|
||||
echo "🔄 Updating dependencies..."
|
||||
|
|
|
|||
|
|
@ -1,54 +1,28 @@
|
|||
"use server";
|
||||
import React from "react";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
||||
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
|
||||
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
||||
|
||||
const pageInfo = {
|
||||
tr: "Birey Sayfası",
|
||||
en: "Individual Page",
|
||||
};
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||
|
||||
export default async function Dashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/individual";
|
||||
const siteUrlsList = (await retrievePageList()) || [];
|
||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
||||
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
// Use the enhanced dashboard hook to get all necessary data
|
||||
const {
|
||||
activePage,
|
||||
searchParamsInstance,
|
||||
lang,
|
||||
PageComponent,
|
||||
siteUrlsList
|
||||
} = await useDashboardPage({
|
||||
pageUrl: "/individual",
|
||||
searchParams
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<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">
|
||||
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default async function DashLayout({
|
|||
}
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './CreateButton';
|
||||
export * from './CustomButtonComponent';
|
||||
export * from './types';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { CardDisplay } from './CardDisplay';
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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]}`;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// Export the main components
|
||||
export { FormDisplay } from "./FormDisplay";
|
||||
|
||||
// Export types
|
||||
export type { FormMode } from "./types";
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export * from './PaginationToolsComponent';
|
||||
export * from './PaginationStats';
|
||||
export * from './PageNavigation';
|
||||
export * from './PageSizeSelector';
|
||||
export * from './types';
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './types';
|
||||
export * from './TextQueryModifier';
|
||||
export * from './SelectQueryModifier';
|
||||
export * from './TypeQueryModifier';
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './SortingComponent';
|
||||
export * from './types';
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
// );
|
||||
|
|
@ -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";
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -236,8 +236,7 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
|||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!notification.read ? "bg-blue-50" : ""
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
|
|
@ -315,8 +314,7 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
|||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
||||
!message.read ? "bg-blue-50" : ""
|
||||
className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -7,9 +7,10 @@ import type { LanguageTranslation } from "./handler";
|
|||
interface NavigationMenuProps {
|
||||
transformedMenu: any[];
|
||||
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
|
||||
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||
|
|
@ -77,16 +78,23 @@ const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }
|
|||
{firstIndex === firstLayerIndex &&
|
||||
secondIndex === secondLayerIndex && (
|
||||
<div className="bg-gray-100 border-t border-gray-200">
|
||||
{subItem.subList.map((subSubItem: any) => (
|
||||
{subItem.subList.map((subSubItem: any) => {
|
||||
// Check if this is the active page
|
||||
const isActive = activePage === subSubItem.siteUrl;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={subSubItem.name}
|
||||
href={subSubItem.siteUrl}
|
||||
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors"
|
||||
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 text-gray-500" />
|
||||
<Home className={`h-4 w-4 mr-2 ${isActive ? "text-emerald-700" : "text-gray-500"}`} />
|
||||
<span>{subSubItem.lg[lang]}</span>
|
||||
</Link>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 t =
|
||||
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
|
||||
|
|
@ -86,7 +86,7 @@ const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
|
|||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} />
|
||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} activePage={activePage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
interface ClientMenuProps {
|
||||
siteUrls: string[];
|
||||
lang?: string;
|
||||
activePage?: string;
|
||||
}
|
||||
|
||||
interface UserSelection {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
const menuPages = {
|
||||
};
|
||||
|
||||
export default menuPages;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
interface ClientMenuProps {
|
||||
lang?: string;
|
||||
activePage: string;
|
||||
}
|
||||
|
||||
interface UserSelection {
|
||||
userType: string;
|
||||
selected: any;
|
||||
}
|
||||
|
||||
export type { ClientMenuProps, UserSelection };
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -1,34 +1,22 @@
|
|||
'use server';
|
||||
import React from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||
|
||||
async function DashboardPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/application";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
const PageComponent = retrievePageByUrl(activePage);
|
||||
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||
pageUrl: "/application",
|
||||
searchParams,
|
||||
});
|
||||
|
||||
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 lang={lang} activePage={activePage} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Header Component */}
|
||||
<Header lang={lang} />
|
||||
<DashboardLayout lang={lang} activePage={activePage}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,23 @@
|
|||
'use server';
|
||||
import React from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||
|
||||
async function PageDashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/dashboard";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
const PageComponent = retrievePageByUrl(activePage);
|
||||
|
||||
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||
pageUrl: "/dashboard",
|
||||
searchParams,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<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} />
|
||||
<DashboardLayout lang={lang} activePage={activePage}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
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 { CreateComponentProps, FieldDefinition } from "./types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
|
@ -13,18 +13,6 @@ 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,
|
||||
|
|
@ -140,13 +128,13 @@ export function CreateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
{errorMessage && (
|
||||
|
|
@ -159,13 +147,13 @@ export function CreateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
rows={3}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
|
|
@ -179,7 +167,7 @@ export function CreateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Select
|
||||
|
|
@ -188,7 +176,7 @@ export function CreateComponent<T>({
|
|||
disabled={field.readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((option) => (
|
||||
|
|
@ -216,7 +204,7 @@ export function CreateComponent<T>({
|
|||
disabled={field.readOnly}
|
||||
/>
|
||||
<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>}
|
||||
</Label>
|
||||
{errorMessage && (
|
||||
|
|
|
|||
|
|
@ -69,13 +69,14 @@ export function FormDisplay<T>({
|
|||
};
|
||||
|
||||
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
|
||||
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}
|
||||
|
|
@ -88,6 +89,7 @@ export function FormDisplay<T>({
|
|||
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}
|
||||
|
|
@ -101,6 +103,7 @@ export function FormDisplay<T>({
|
|||
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}
|
||||
|
|
@ -108,7 +111,7 @@ export function FormDisplay<T>({
|
|||
onCancel={onCancel}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
formProps={formProps}
|
||||
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
|
||||
/>
|
||||
) : null;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
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 { UpdateComponentProps, FieldDefinition } from "./types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
|
@ -13,18 +13,6 @@ 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,
|
||||
|
|
@ -154,13 +142,13 @@ export function UpdateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
{errorMessage && (
|
||||
|
|
@ -173,13 +161,13 @@ export function UpdateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
rows={3}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
|
|
@ -193,7 +181,7 @@ export function UpdateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Select
|
||||
|
|
@ -202,7 +190,7 @@ export function UpdateComponent<T>({
|
|||
disabled={field.readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((option) => (
|
||||
|
|
@ -230,7 +218,7 @@ export function UpdateComponent<T>({
|
|||
disabled={field.readOnly}
|
||||
/>
|
||||
<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>}
|
||||
</Label>
|
||||
{errorMessage && (
|
||||
|
|
@ -243,7 +231,7 @@ export function UpdateComponent<T>({
|
|||
return (
|
||||
<div className="space-y-2" key={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>}
|
||||
</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -2,21 +2,10 @@
|
|||
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 { ViewComponentProps, FieldDefinition } 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) =>
|
||||
|
|
@ -103,7 +92,7 @@ const ViewFieldGroup: React.FC<{
|
|||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={value}
|
||||
label={field.label}
|
||||
label={field.label[lang as "en" | "tr"]}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
hasError={hasError}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,17 @@
|
|||
"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
|
||||
export type FormMode = "list" | "create" | "update" | "view";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -11,23 +12,25 @@ export function useApiData<T>(
|
|||
initialParams: Partial<RequestParams> = {}
|
||||
) {
|
||||
// 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 {
|
||||
// Prepare the request body with action and all params
|
||||
const requestBody = {
|
||||
action: 'list',
|
||||
action: "list",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
orderField: params.orderField,
|
||||
orderType: params.orderType,
|
||||
query: params.query
|
||||
query: params.query,
|
||||
};
|
||||
|
||||
// Make the API request using POST
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,22 +1,5 @@
|
|||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
|
||||
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;
|
||||
}
|
||||
import { RequestParams, ResponseMetadata } from "../schemas";
|
||||
|
||||
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
||||
|
||||
|
|
@ -110,7 +93,9 @@ export function useDataFetching<T>(
|
|||
|
||||
useEffect(() => {
|
||||
// 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) {
|
||||
const timer = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// );
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -16,7 +16,10 @@ import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSele
|
|||
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||
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
|
||||
const {
|
||||
data,
|
||||
|
|
@ -128,25 +131,24 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
|||
};
|
||||
|
||||
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">
|
||||
<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">
|
||||
{/* Grid Selection */}
|
||||
<GridSelectionComponent
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
<div>
|
||||
|
||||
{/* Language Selection */}
|
||||
<LanguageSelectionComponent
|
||||
lang={lang as Language}
|
||||
setLang={(newLang) => console.log("Language change not implemented", newLang)}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
setLang={setLang}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === "list" ? (
|
||||
<>
|
||||
|
|
@ -174,14 +176,14 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
|||
<Filter className="mr-2 h-4 w-4" />
|
||||
{translations[lang].filterSelection || "Filter Selection"}
|
||||
</div>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleResetAllFilters}
|
||||
className="text-xs"
|
||||
>
|
||||
{translations[lang].resetAll || "Reset All"}
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,19 @@
|
|||
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
|
||||
const ApplicationBaseSchema = z.object({
|
||||
|
|
@ -22,6 +37,34 @@ const ApplicationBaseSchema = z.object({
|
|||
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
|
||||
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
||||
uu_id: true,
|
||||
|
|
@ -50,31 +93,6 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
|||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||
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
|
||||
const baseFieldDefinitions = {
|
||||
// Identification fields
|
||||
|
|
@ -82,11 +100,30 @@ const baseFieldDefinitions = {
|
|||
title: "Identification Information",
|
||||
order: 1,
|
||||
fields: {
|
||||
uu_id: { type: "text", label: "UUID", readOnly: true, required: false },
|
||||
name: { type: "text", label: "Name", readOnly: false, required: true },
|
||||
uu_id: {
|
||||
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: {
|
||||
type: "text",
|
||||
label: "Application Code",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_code,
|
||||
en: ApplicationBaseTranslationEn.application_code,
|
||||
},
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
|
|
@ -100,27 +137,39 @@ const baseFieldDefinitions = {
|
|||
fields: {
|
||||
site_url: {
|
||||
type: "text",
|
||||
label: "Site URL",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.site_url,
|
||||
en: ApplicationBaseTranslationEn.site_url,
|
||||
},
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
application_type: {
|
||||
type: "select",
|
||||
label: "Application Type",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_type,
|
||||
en: ApplicationBaseTranslationEn.application_type,
|
||||
},
|
||||
options: ["info", "Dash", "Admin"],
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
application_for: {
|
||||
type: "select",
|
||||
label: "Application For",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_for,
|
||||
en: ApplicationBaseTranslationEn.application_for,
|
||||
},
|
||||
options: ["EMP", "OCC"],
|
||||
readOnly: false,
|
||||
required: false,
|
||||
},
|
||||
description: {
|
||||
type: "textarea",
|
||||
label: "Description",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.description,
|
||||
en: ApplicationBaseTranslationEn.description,
|
||||
},
|
||||
readOnly: false,
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -134,14 +183,20 @@ const baseFieldDefinitions = {
|
|||
fields: {
|
||||
active: {
|
||||
type: "checkbox",
|
||||
label: "Active",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.active,
|
||||
en: ApplicationBaseTranslationEn.active,
|
||||
},
|
||||
readOnly: false,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
deleted: {
|
||||
type: "checkbox",
|
||||
label: "Deleted",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.deleted,
|
||||
en: ApplicationBaseTranslationEn.deleted,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
|
|
@ -156,13 +211,19 @@ const baseFieldDefinitions = {
|
|||
fields: {
|
||||
created_at: {
|
||||
type: "date",
|
||||
label: "Created At",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.created_at,
|
||||
en: ApplicationBaseTranslationEn.created_at,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
},
|
||||
updated_at: {
|
||||
type: "date",
|
||||
label: "Updated At",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.updated_at,
|
||||
en: ApplicationBaseTranslationEn.updated_at,
|
||||
},
|
||||
readOnly: true,
|
||||
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
|
||||
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||
|
||||
|
|
@ -386,6 +425,3 @@ export const fieldsByMode = {
|
|||
update: Object.keys(updateFieldDefinitions),
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
services:
|
||||
# client_frontend:
|
||||
# container_name: client_frontend
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: WebServices/client-frontend/Dockerfile
|
||||
# networks:
|
||||
# - wag-services
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# environment:
|
||||
# - NODE_ENV=development
|
||||
# cpus: 1
|
||||
# mem_limit: 2048m
|
||||
client_frontend:
|
||||
container_name: client_frontend
|
||||
build:
|
||||
context: .
|
||||
dockerfile: WebServices/client-frontend/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
cpus: 1
|
||||
mem_limit: 2048m
|
||||
|
||||
# volumes:
|
||||
# - client-frontend:/WebServices/client-frontend
|
||||
|
|
@ -57,32 +57,32 @@ services:
|
|||
mem_limit: 512m
|
||||
cpus: 0.5
|
||||
|
||||
# identity_service:
|
||||
# container_name: identity_service
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ApiServices/IdentityService/Dockerfile
|
||||
# networks:
|
||||
# - wag-services
|
||||
# depends_on:
|
||||
# - initializer_service
|
||||
# env_file:
|
||||
# - api_env.env
|
||||
# environment:
|
||||
# - API_PATH=app:app
|
||||
# - API_HOST=0.0.0.0
|
||||
# - API_PORT=8002
|
||||
# - API_LOG_LEVEL=info
|
||||
# - API_RELOAD=1
|
||||
# - API_APP_NAME=evyos-identity-api-gateway
|
||||
# - API_TITLE=WAG API Identity Api Gateway
|
||||
# - 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_APP_URL=https://identity_service
|
||||
# ports:
|
||||
# - "8002:8002"
|
||||
# mem_limit: 512m
|
||||
# cpus: 0.5
|
||||
identity_service:
|
||||
container_name: identity_service
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ApiServices/IdentityService/Dockerfile
|
||||
networks:
|
||||
- wag-services
|
||||
depends_on:
|
||||
- initializer_service
|
||||
env_file:
|
||||
- api_env.env
|
||||
environment:
|
||||
- API_PATH=app:app
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8002
|
||||
- API_LOG_LEVEL=info
|
||||
- API_RELOAD=1
|
||||
- API_APP_NAME=evyos-identity-api-gateway
|
||||
- API_TITLE=WAG API Identity Api Gateway
|
||||
- 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_APP_URL=https://identity_service
|
||||
ports:
|
||||
- "8002:8002"
|
||||
mem_limit: 512m
|
||||
cpus: 0.5
|
||||
|
||||
# building_service:
|
||||
# container_name: building_service
|
||||
|
|
|
|||
Loading…
Reference in New Issue