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 date-picker -y
|
||||||
npx shadcn@latest add skeleton -y
|
npx shadcn@latest add skeleton -y
|
||||||
npx shadcn@latest add table -y
|
npx shadcn@latest add table -y
|
||||||
|
npx shadcn@latest add textarea -y
|
||||||
|
|
||||||
# Update any dependencies with legacy peer deps
|
# Update any dependencies with legacy peer deps
|
||||||
echo "🔄 Updating dependencies..."
|
echo "🔄 Updating dependencies..."
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,28 @@
|
||||||
"use server";
|
"use server";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever";
|
|
||||||
import { searchPlaceholder } from "@/app/commons/pageDefaults";
|
|
||||||
|
|
||||||
const pageInfo = {
|
|
||||||
tr: "Birey Sayfası",
|
|
||||||
en: "Individual Page",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Dashboard({
|
export default async function Dashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const activePage = "/individual";
|
// Use the enhanced dashboard hook to get all necessary data
|
||||||
const siteUrlsList = (await retrievePageList()) || [];
|
const {
|
||||||
const pageToDirect = await retrievePagebyUrl(activePage);
|
activePage,
|
||||||
const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage);
|
searchParamsInstance,
|
||||||
const searchParamsInstance = await searchParams;
|
lang,
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
PageComponent,
|
||||||
|
siteUrlsList
|
||||||
|
} = await useDashboardPage({
|
||||||
|
pageUrl: "/individual",
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage} siteUrls={siteUrlsList}>
|
||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
{/* Sidebar */}
|
</DashboardLayout>
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
|
||||||
<ClientMenu siteUrls={siteUrlsList} lang={lang} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Sticky Header */}
|
|
||||||
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
|
|
||||||
<h1 className="text-2xl font-semibold">{pageInfo[lang]}</h1>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={searchPlaceholder[lang]}
|
|
||||||
className="border px-3 py-2 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="p-4 overflow-y-auto">
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default async function DashLayout({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="h-full w-full overflow-y-auto">{children}</div>
|
<div className="h-full w-full">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,9 +236,8 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||||
notifications.map((notification) => (
|
notifications.map((notification) => (
|
||||||
<div
|
<div
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
className={`px-4 py-2 border-b last:border-b-0 ${!notification.read ? "bg-blue-50" : ""
|
||||||
!notification.read ? "bg-blue-50" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<h4 className="text-sm font-semibold">
|
<h4 className="text-sm font-semibold">
|
||||||
|
|
@ -315,9 +314,8 @@ const Header: React.FC<HeaderProps> = ({ lang }) => {
|
||||||
messages.map((message) => (
|
messages.map((message) => (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`px-4 py-2 border-b last:border-b-0 ${
|
className={`px-4 py-2 border-b last:border-b-0 ${!message.read ? "bg-blue-50" : ""
|
||||||
!message.read ? "bg-blue-50" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
interface NavigationMenuProps {
|
||||||
transformedMenu: any[];
|
transformedMenu: any[];
|
||||||
lang: string;
|
lang: string;
|
||||||
|
activePage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }) => {
|
const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang, activePage }) => {
|
||||||
// State to track which menu items are expanded
|
// State to track which menu items are expanded
|
||||||
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
const [firstLayerIndex, setFirstLayerIndex] = useState<number>(-1);
|
||||||
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
const [secondLayerIndex, setSecondLayerIndex] = useState<number>(-1);
|
||||||
|
|
@ -77,16 +78,23 @@ const NavigationMenu: React.FC<NavigationMenuProps> = ({ transformedMenu, lang }
|
||||||
{firstIndex === firstLayerIndex &&
|
{firstIndex === firstLayerIndex &&
|
||||||
secondIndex === secondLayerIndex && (
|
secondIndex === secondLayerIndex && (
|
||||||
<div className="bg-gray-100 border-t border-gray-200">
|
<div className="bg-gray-100 border-t border-gray-200">
|
||||||
{subItem.subList.map((subSubItem: any) => (
|
{subItem.subList.map((subSubItem: any) => {
|
||||||
<Link
|
// Check if this is the active page
|
||||||
key={subSubItem.name}
|
const isActive = activePage === subSubItem.siteUrl;
|
||||||
href={subSubItem.siteUrl}
|
|
||||||
className="flex items-center w-full p-3 pl-12 text-left text-gray-700 hover:bg-gray-200 transition-colors"
|
return (
|
||||||
>
|
<Link
|
||||||
<Home className="h-4 w-4 mr-2 text-gray-500" />
|
key={subSubItem.name}
|
||||||
<span>{subSubItem.lg[lang]}</span>
|
href={subSubItem.siteUrl}
|
||||||
</Link>
|
className={`flex items-center w-full p-3 pl-12 text-left transition-colors ${isActive
|
||||||
))}
|
? "bg-emerald-200 text-emerald-900 font-medium"
|
||||||
|
: "text-gray-700 hover:bg-gray-200"}`}
|
||||||
|
>
|
||||||
|
<Home className={`h-4 w-4 mr-2 ${isActive ? "text-emerald-700" : "text-gray-500"}`} />
|
||||||
|
<span>{subSubItem.lg[lang]}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const dashboardLanguage = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
|
const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en", activePage }) => {
|
||||||
const transformedMenu = transformMenu(siteUrls);
|
const transformedMenu = transformMenu(siteUrls);
|
||||||
const t =
|
const t =
|
||||||
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
|
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
|
||||||
|
|
@ -86,7 +86,7 @@ const ClientMenu: React.FC<ClientMenuProps> = ({ siteUrls, lang = "en" }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Menu */}
|
{/* Navigation Menu */}
|
||||||
<NavigationMenu transformedMenu={transformedMenu} lang={lang} />
|
<NavigationMenu transformedMenu={transformedMenu} lang={lang} activePage={activePage} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function retrievePageByUrlAndPageId(
|
||||||
try {
|
try {
|
||||||
const PageComponent =
|
const PageComponent =
|
||||||
PageNavigator[pageUrl as keyof typeof PageNavigator][
|
PageNavigator[pageUrl as keyof typeof PageNavigator][
|
||||||
pageId as keyof (typeof PageNavigator)[typeof pageUrl]
|
pageId as keyof (typeof PageNavigator)[typeof pageUrl]
|
||||||
];
|
];
|
||||||
if (!PageComponent) {
|
if (!PageComponent) {
|
||||||
console.log(`Page component not found for pageId: ${pageId}`);
|
console.log(`Page component not found for pageId: ${pageId}`);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
interface ClientMenuProps {
|
||||||
siteUrls: string[];
|
siteUrls: string[];
|
||||||
lang?: string;
|
lang?: string;
|
||||||
|
activePage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSelection {
|
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 React from "react";
|
||||||
import Header from "@/components/header/Header";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
|
||||||
|
|
||||||
async function DashboardPage({
|
async function DashboardPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const activePage = "/application";
|
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||||
const searchParamsInstance = await searchParams;
|
pageUrl: "/application",
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
searchParams,
|
||||||
const PageComponent = retrievePageByUrl(activePage);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage}>
|
||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
{/* Sidebar */}
|
</DashboardLayout>
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
|
||||||
<ClientMenu lang={lang} activePage={activePage} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
|
||||||
{/* Header Component */}
|
|
||||||
<Header lang={lang} />
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,23 @@
|
||||||
|
'use server';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Header from "@/components/header/Header";
|
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
|
||||||
|
|
||||||
async function PageDashboard({
|
async function PageDashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||||
}) {
|
}) {
|
||||||
const activePage = "/dashboard";
|
|
||||||
const searchParamsInstance = await searchParams;
|
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
pageUrl: "/dashboard",
|
||||||
const PageComponent = retrievePageByUrl(activePage);
|
searchParams,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLayout lang={lang} activePage={activePage}>
|
||||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||||
{/* Sidebar */}
|
</DashboardLayout>
|
||||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
|
||||||
<ClientMenu lang={lang} activePage={activePage} />
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
{/* Header Component */}
|
|
||||||
<Header lang={lang} />
|
|
||||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { CreateComponentProps } from "./types";
|
import { CreateComponentProps, FieldDefinition } from "./types";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
// Import field definitions type
|
|
||||||
interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: string;
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateComponent<T>({
|
export function CreateComponent<T>({
|
||||||
refetch,
|
refetch,
|
||||||
setMode,
|
setMode,
|
||||||
|
|
@ -140,13 +128,13 @@ export function CreateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={fieldName}
|
id={fieldName}
|
||||||
{...register(fieldName)}
|
{...register(fieldName)}
|
||||||
placeholder={t[fieldName] || field.label}
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|
@ -159,13 +147,13 @@ export function CreateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id={fieldName}
|
id={fieldName}
|
||||||
{...register(fieldName)}
|
{...register(fieldName)}
|
||||||
placeholder={t[fieldName] || field.label}
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
@ -179,7 +167,7 @@ export function CreateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -188,7 +176,7 @@ export function CreateComponent<T>({
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{field.options?.map((option) => (
|
{field.options?.map((option) => (
|
||||||
|
|
@ -216,7 +204,7 @@ export function CreateComponent<T>({
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,14 @@ export function FormDisplay<T>({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSchemaDefinitions();
|
loadSchemaDefinitions();
|
||||||
}, [formProps, mode]);
|
}, [formProps, mode, lang]); // Added lang as a dependency to ensure re-fetch when language changes
|
||||||
|
|
||||||
// Render the appropriate component based on the mode
|
// Render the appropriate component based on the mode
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "create":
|
case "create":
|
||||||
return (
|
return (
|
||||||
<CreateComponent<T>
|
<CreateComponent<T>
|
||||||
|
key={`create-${lang}`} // Add key with lang to force re-render on language change
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
setSelectedItem={setSelectedItem}
|
setSelectedItem={setSelectedItem}
|
||||||
|
|
@ -88,6 +89,7 @@ export function FormDisplay<T>({
|
||||||
case "update":
|
case "update":
|
||||||
return initialData ? (
|
return initialData ? (
|
||||||
<UpdateComponent<T>
|
<UpdateComponent<T>
|
||||||
|
key={`update-${lang}`} // Add key with lang to force re-render on language change
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
|
|
@ -101,6 +103,7 @@ export function FormDisplay<T>({
|
||||||
case "view":
|
case "view":
|
||||||
return initialData ? (
|
return initialData ? (
|
||||||
<ViewComponent<T>
|
<ViewComponent<T>
|
||||||
|
key={`view-${lang}`} // Add key with lang to force re-render on language change
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
|
|
@ -108,7 +111,7 @@ export function FormDisplay<T>({
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
formProps={formProps}
|
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { UpdateComponentProps } from "./types";
|
import { UpdateComponentProps, FieldDefinition } from "./types";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
// Import field definitions type
|
|
||||||
interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: string;
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
name?: string; // Add name property for TypeScript compatibility
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdateComponent<T>({
|
export function UpdateComponent<T>({
|
||||||
initialData,
|
initialData,
|
||||||
refetch,
|
refetch,
|
||||||
|
|
@ -154,13 +142,13 @@ export function UpdateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={fieldName}
|
id={fieldName}
|
||||||
{...register(fieldName)}
|
{...register(fieldName)}
|
||||||
placeholder={t[fieldName] || field.label}
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|
@ -173,13 +161,13 @@ export function UpdateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id={fieldName}
|
id={fieldName}
|
||||||
{...register(fieldName)}
|
{...register(fieldName)}
|
||||||
placeholder={t[fieldName] || field.label}
|
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
@ -193,7 +181,7 @@ export function UpdateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -202,7 +190,7 @@ export function UpdateComponent<T>({
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{field.options?.map((option) => (
|
{field.options?.map((option) => (
|
||||||
|
|
@ -230,7 +218,7 @@ export function UpdateComponent<T>({
|
||||||
disabled={field.readOnly}
|
disabled={field.readOnly}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|
@ -243,7 +231,7 @@ export function UpdateComponent<T>({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" key={fieldName}>
|
<div className="space-y-2" key={fieldName}>
|
||||||
<Label htmlFor={fieldName}>
|
<Label htmlFor={fieldName}>
|
||||||
{t[fieldName] || field.label}
|
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,10 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { ViewComponentProps } from "./types";
|
import { ViewComponentProps, FieldDefinition } from "./types";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// Import field definitions type
|
|
||||||
export interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: string;
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
name?: string; // Add name property for TypeScript compatibility
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to format field label
|
// Utility function to format field label
|
||||||
const formatFieldLabel = (fieldName: string) =>
|
const formatFieldLabel = (fieldName: string) =>
|
||||||
|
|
@ -103,7 +92,7 @@ const ViewFieldGroup: React.FC<{
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
value={value}
|
value={value}
|
||||||
label={field.label}
|
label={field.label[lang as "en" | "tr"]}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
// Import field definitions type
|
||||||
|
export interface FieldDefinition {
|
||||||
|
type: string;
|
||||||
|
group: string;
|
||||||
|
label: { tr: string; en: string };
|
||||||
|
options?: string[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Define the FormMode type to ensure consistency
|
// Define the FormMode type to ensure consistency
|
||||||
export type FormMode = "list" | "create" | "update" | "view";
|
export type FormMode = "list" | "create" | "update" | "view";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching";
|
import { useDataFetching, ApiResponse } from "./useDataFetching";
|
||||||
|
import { RequestParams } from "../schemas";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for fetching data from Next.js API routes
|
* Hook for fetching data from Next.js API routes
|
||||||
|
|
@ -11,35 +12,37 @@ export function useApiData<T>(
|
||||||
initialParams: Partial<RequestParams> = {}
|
initialParams: Partial<RequestParams> = {}
|
||||||
) {
|
) {
|
||||||
// Define the fetch function that will be passed to useDataFetching
|
// Define the fetch function that will be passed to useDataFetching
|
||||||
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => {
|
const fetchFromApi = async (
|
||||||
|
params: RequestParams
|
||||||
|
): Promise<ApiResponse<T>> => {
|
||||||
try {
|
try {
|
||||||
// Prepare the request body with action and all params
|
// Prepare the request body with action and all params
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
action: 'list',
|
action: "list",
|
||||||
page: params.page,
|
page: params.page,
|
||||||
size: params.size,
|
size: params.size,
|
||||||
orderField: params.orderField,
|
orderField: params.orderField,
|
||||||
orderType: params.orderType,
|
orderType: params.orderType,
|
||||||
query: params.query
|
query: params.query,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make the API request using POST
|
// Make the API request using POST
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data from API:", error);
|
console.error("Error fetching data from API:", error);
|
||||||
|
|
||||||
// Return empty data with pagination info on error
|
// Return empty data with pagination info on error
|
||||||
return {
|
return {
|
||||||
data: [],
|
data: [],
|
||||||
|
|
|
||||||
|
|
@ -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";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { RequestParams, ResponseMetadata } from "../schemas";
|
||||||
export interface RequestParams {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
orderField: string[];
|
|
||||||
orderType: string[];
|
|
||||||
query: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResponseMetadata {
|
|
||||||
totalCount: number;
|
|
||||||
totalItems: number;
|
|
||||||
totalPages: number;
|
|
||||||
pageCount: number;
|
|
||||||
allCount?: number;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
||||||
|
|
||||||
|
|
@ -104,19 +87,21 @@ export function useDataFetching<T>(
|
||||||
|
|
||||||
// Track if this is the initial mount
|
// Track if this is the initial mount
|
||||||
const initialMountRef = useRef(true);
|
const initialMountRef = useRef(true);
|
||||||
|
|
||||||
// Track previous request params to avoid unnecessary fetches
|
// Track previous request params to avoid unnecessary fetches
|
||||||
const prevRequestParamsRef = useRef<RequestParams>(requestParams);
|
const prevRequestParamsRef = useRef<RequestParams>(requestParams);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch on mount or when request params actually change
|
// Only fetch on mount or when request params actually change
|
||||||
const paramsChanged = JSON.stringify(prevRequestParamsRef.current) !== JSON.stringify(requestParams);
|
const paramsChanged =
|
||||||
|
JSON.stringify(prevRequestParamsRef.current) !==
|
||||||
|
JSON.stringify(requestParams);
|
||||||
|
|
||||||
if (initialMountRef.current || paramsChanged) {
|
if (initialMountRef.current || paramsChanged) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
fetchDataFromApi();
|
fetchDataFromApi();
|
||||||
initialMountRef.current = false;
|
initialMountRef.current = false;
|
||||||
prevRequestParamsRef.current = {...requestParams};
|
prevRequestParamsRef.current = { ...requestParams };
|
||||||
}, 300); // Debounce
|
}, 300); // Debounce
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
|
||||||
|
|
@ -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 { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||||
import type { FormMode } from "@/components/common/FormDisplay/types";
|
import type { FormMode } from "@/components/common/FormDisplay/types";
|
||||||
|
|
||||||
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||||
|
// Add local state for language to ensure it persists when changed
|
||||||
|
const [lang, setLang] = useState<Language>(initialLang as Language);
|
||||||
|
|
||||||
// Use the API data hook directly
|
// Use the API data hook directly
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
|
|
@ -128,23 +131,22 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4 overflow-y-auto" >
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1>
|
<h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex space-x-4">
|
||||||
<GridSelectionComponent
|
{/* Grid Selection */}
|
||||||
gridCols={gridCols}
|
<GridSelectionComponent
|
||||||
setGridCols={setGridCols}
|
gridCols={gridCols}
|
||||||
translations={translations}
|
setGridCols={setGridCols}
|
||||||
lang={lang}
|
/>
|
||||||
|
|
||||||
|
{/* Language Selection */}
|
||||||
|
<LanguageSelectionComponent
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
setLang={setLang}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<LanguageSelectionComponent
|
|
||||||
lang={lang as Language}
|
|
||||||
setLang={(newLang) => console.log("Language change not implemented", newLang)}
|
|
||||||
translations={translations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -174,14 +176,14 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
<Filter className="mr-2 h-4 w-4" />
|
||||||
{translations[lang].filterSelection || "Filter Selection"}
|
{translations[lang].filterSelection || "Filter Selection"}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{/* <Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleResetAllFilters}
|
onClick={handleResetAllFilters}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{translations[lang].resetAll || "Reset All"}
|
{translations[lang].resetAll || "Reset All"}
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,19 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { flattenFieldDefinitions } from "../schemas/zodSchemas";
|
||||||
|
|
||||||
|
export interface ApplicationData {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
application_for?: string;
|
||||||
|
description?: string;
|
||||||
|
active: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Base schema with all possible fields
|
// Base schema with all possible fields
|
||||||
const ApplicationBaseSchema = z.object({
|
const ApplicationBaseSchema = z.object({
|
||||||
|
|
@ -22,6 +37,34 @@ const ApplicationBaseSchema = z.object({
|
||||||
updated_at: z.string().optional(),
|
updated_at: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ApplicationBaseTranslationTr = {
|
||||||
|
uu_id: "UUID",
|
||||||
|
name: "Name",
|
||||||
|
application_code: "Application Code",
|
||||||
|
site_url: "Site URL",
|
||||||
|
application_type: "Application Type",
|
||||||
|
application_for: "Application For",
|
||||||
|
description: "Description",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApplicationBaseTranslationEn = {
|
||||||
|
uu_id: "UUID",
|
||||||
|
name: "Name",
|
||||||
|
application_code: "Application Code",
|
||||||
|
site_url: "Site URL",
|
||||||
|
application_type: "Application Type",
|
||||||
|
application_for: "Application For",
|
||||||
|
description: "Description",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
// Schema for creating a new application
|
// Schema for creating a new application
|
||||||
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
||||||
uu_id: true,
|
uu_id: true,
|
||||||
|
|
@ -50,31 +93,6 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
||||||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||||
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||||
|
|
||||||
// Define field definition type
|
|
||||||
export interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: string;
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationData {
|
|
||||||
id?: number;
|
|
||||||
name: string;
|
|
||||||
application_code: string;
|
|
||||||
site_url: string;
|
|
||||||
application_type: string;
|
|
||||||
application_for?: string;
|
|
||||||
description?: string;
|
|
||||||
active: boolean;
|
|
||||||
deleted?: boolean;
|
|
||||||
created_at?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base field definitions grouped by section
|
// Base field definitions grouped by section
|
||||||
const baseFieldDefinitions = {
|
const baseFieldDefinitions = {
|
||||||
// Identification fields
|
// Identification fields
|
||||||
|
|
@ -82,11 +100,30 @@ const baseFieldDefinitions = {
|
||||||
title: "Identification Information",
|
title: "Identification Information",
|
||||||
order: 1,
|
order: 1,
|
||||||
fields: {
|
fields: {
|
||||||
uu_id: { type: "text", label: "UUID", readOnly: true, required: false },
|
uu_id: {
|
||||||
name: { type: "text", label: "Name", readOnly: false, required: true },
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.uu_id,
|
||||||
|
en: ApplicationBaseTranslationEn.uu_id,
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.name,
|
||||||
|
en: ApplicationBaseTranslationEn.name,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
application_code: {
|
application_code: {
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Application Code",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.application_code,
|
||||||
|
en: ApplicationBaseTranslationEn.application_code,
|
||||||
|
},
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
@ -100,27 +137,39 @@ const baseFieldDefinitions = {
|
||||||
fields: {
|
fields: {
|
||||||
site_url: {
|
site_url: {
|
||||||
type: "text",
|
type: "text",
|
||||||
label: "Site URL",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.site_url,
|
||||||
|
en: ApplicationBaseTranslationEn.site_url,
|
||||||
|
},
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
application_type: {
|
application_type: {
|
||||||
type: "select",
|
type: "select",
|
||||||
label: "Application Type",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.application_type,
|
||||||
|
en: ApplicationBaseTranslationEn.application_type,
|
||||||
|
},
|
||||||
options: ["info", "Dash", "Admin"],
|
options: ["info", "Dash", "Admin"],
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
application_for: {
|
application_for: {
|
||||||
type: "select",
|
type: "select",
|
||||||
label: "Application For",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.application_for,
|
||||||
|
en: ApplicationBaseTranslationEn.application_for,
|
||||||
|
},
|
||||||
options: ["EMP", "OCC"],
|
options: ["EMP", "OCC"],
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "textarea",
|
type: "textarea",
|
||||||
label: "Description",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.description,
|
||||||
|
en: ApplicationBaseTranslationEn.description,
|
||||||
|
},
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
@ -134,14 +183,20 @@ const baseFieldDefinitions = {
|
||||||
fields: {
|
fields: {
|
||||||
active: {
|
active: {
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
label: "Active",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.active,
|
||||||
|
en: ApplicationBaseTranslationEn.active,
|
||||||
|
},
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
required: false,
|
required: false,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
deleted: {
|
deleted: {
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
label: "Deleted",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.deleted,
|
||||||
|
en: ApplicationBaseTranslationEn.deleted,
|
||||||
|
},
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
required: false,
|
required: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
@ -156,13 +211,19 @@ const baseFieldDefinitions = {
|
||||||
fields: {
|
fields: {
|
||||||
created_at: {
|
created_at: {
|
||||||
type: "date",
|
type: "date",
|
||||||
label: "Created At",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.created_at,
|
||||||
|
en: ApplicationBaseTranslationEn.created_at,
|
||||||
|
},
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
updated_at: {
|
updated_at: {
|
||||||
type: "date",
|
type: "date",
|
||||||
label: "Updated At",
|
label: {
|
||||||
|
tr: ApplicationBaseTranslationTr.updated_at,
|
||||||
|
en: ApplicationBaseTranslationEn.updated_at,
|
||||||
|
},
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
@ -170,28 +231,6 @@ const baseFieldDefinitions = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to flatten grouped field definitions into a flat structure
|
|
||||||
const flattenFieldDefinitions = (
|
|
||||||
groupedDefs: any
|
|
||||||
): Record<string, FieldDefinition> => {
|
|
||||||
const result: Record<string, FieldDefinition> = {};
|
|
||||||
|
|
||||||
Object.entries(groupedDefs).forEach(
|
|
||||||
([groupName, groupConfig]: [string, any]) => {
|
|
||||||
Object.entries(groupConfig.fields).forEach(
|
|
||||||
([fieldName, fieldConfig]: [string, any]) => {
|
|
||||||
result[fieldName] = {
|
|
||||||
...fieldConfig,
|
|
||||||
group: groupName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a flat version of the field definitions for compatibility
|
// Create a flat version of the field definitions for compatibility
|
||||||
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||||
|
|
||||||
|
|
@ -386,6 +425,3 @@ export const fieldsByMode = {
|
||||||
update: Object.keys(updateFieldDefinitions),
|
update: Object.keys(updateFieldDefinitions),
|
||||||
view: Object.keys(viewFieldDefinitions),
|
view: Object.keys(viewFieldDefinitions),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Note: Direct fetch function has been removed to use the API route instead
|
|
||||||
// Data fetching is now handled in the hooks.ts file using the POST endpoint
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
services:
|
||||||
# client_frontend:
|
client_frontend:
|
||||||
# container_name: client_frontend
|
container_name: client_frontend
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: WebServices/client-frontend/Dockerfile
|
dockerfile: WebServices/client-frontend/Dockerfile
|
||||||
# networks:
|
networks:
|
||||||
# - wag-services
|
- wag-services
|
||||||
# ports:
|
ports:
|
||||||
# - "3000:3000"
|
- "3000:3000"
|
||||||
# environment:
|
environment:
|
||||||
# - NODE_ENV=development
|
- NODE_ENV=development
|
||||||
# cpus: 1
|
cpus: 1
|
||||||
# mem_limit: 2048m
|
mem_limit: 2048m
|
||||||
|
|
||||||
# volumes:
|
# volumes:
|
||||||
# - client-frontend:/WebServices/client-frontend
|
# - client-frontend:/WebServices/client-frontend
|
||||||
|
|
@ -57,32 +57,32 @@ services:
|
||||||
mem_limit: 512m
|
mem_limit: 512m
|
||||||
cpus: 0.5
|
cpus: 0.5
|
||||||
|
|
||||||
# identity_service:
|
identity_service:
|
||||||
# container_name: identity_service
|
container_name: identity_service
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: ApiServices/IdentityService/Dockerfile
|
dockerfile: ApiServices/IdentityService/Dockerfile
|
||||||
# networks:
|
networks:
|
||||||
# - wag-services
|
- wag-services
|
||||||
# depends_on:
|
depends_on:
|
||||||
# - initializer_service
|
- initializer_service
|
||||||
# env_file:
|
env_file:
|
||||||
# - api_env.env
|
- api_env.env
|
||||||
# environment:
|
environment:
|
||||||
# - API_PATH=app:app
|
- API_PATH=app:app
|
||||||
# - API_HOST=0.0.0.0
|
- API_HOST=0.0.0.0
|
||||||
# - API_PORT=8002
|
- API_PORT=8002
|
||||||
# - API_LOG_LEVEL=info
|
- API_LOG_LEVEL=info
|
||||||
# - API_RELOAD=1
|
- API_RELOAD=1
|
||||||
# - API_APP_NAME=evyos-identity-api-gateway
|
- API_APP_NAME=evyos-identity-api-gateway
|
||||||
# - API_TITLE=WAG API Identity Api Gateway
|
- API_TITLE=WAG API Identity Api Gateway
|
||||||
# - API_FORGOT_LINK=https://identity_service/forgot-password
|
- API_FORGOT_LINK=https://identity_service/forgot-password
|
||||||
# - API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
|
||||||
# - API_APP_URL=https://identity_service
|
- API_APP_URL=https://identity_service
|
||||||
# ports:
|
ports:
|
||||||
# - "8002:8002"
|
- "8002:8002"
|
||||||
# mem_limit: 512m
|
mem_limit: 512m
|
||||||
# cpus: 0.5
|
cpus: 0.5
|
||||||
|
|
||||||
# building_service:
|
# building_service:
|
||||||
# container_name: building_service
|
# container_name: building_service
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue