updated lang change and FormDisplay Components
This commit is contained in:
@@ -1,34 +1,22 @@
|
||||
'use server';
|
||||
import React from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||
|
||||
async function DashboardPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/application";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
const PageComponent = retrievePageByUrl(activePage);
|
||||
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||
pageUrl: "/application",
|
||||
searchParams,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||
<ClientMenu lang={lang} activePage={activePage} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4 overflow-y-auto">
|
||||
{/* Header Component */}
|
||||
<Header lang={lang} />
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<DashboardLayout lang={lang} activePage={activePage}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
'use server';
|
||||
import React from "react";
|
||||
import Header from "@/components/header/Header";
|
||||
import ClientMenu from "@/components/menu/menu";
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
import { useDashboardPage } from "@/components/common/hooks/useDashboardPage";
|
||||
|
||||
async function PageDashboard({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/dashboard";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
const PageComponent = retrievePageByUrl(activePage);
|
||||
|
||||
const { activePage, searchParamsInstance, lang, PageComponent } = await useDashboardPage({
|
||||
pageUrl: "/dashboard",
|
||||
searchParams,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-1/4 border-r p-4 overflow-y-auto">
|
||||
<ClientMenu lang={lang} activePage={activePage} />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col w-3/4">
|
||||
{/* Header Component */}
|
||||
<Header lang={lang} />
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<DashboardLayout lang={lang} activePage={activePage}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// Translations for error messages
|
||||
const translations = {
|
||||
en: {
|
||||
errorTitle: 'An error occurred',
|
||||
errorMessage: 'Sorry, something went wrong while loading this page.',
|
||||
unknownError: 'Unknown error',
|
||||
errorId: 'Error ID',
|
||||
tryAgain: 'Try again',
|
||||
goHome: 'Go to Home'
|
||||
},
|
||||
tr: {
|
||||
errorTitle: 'Bir hata oluştu',
|
||||
errorMessage: 'Üzgünüz, bu sayfa yüklenirken bir sorun oluştu.',
|
||||
unknownError: 'Bilinmeyen hata',
|
||||
errorId: 'Hata ID',
|
||||
tryAgain: 'Tekrar dene',
|
||||
goHome: 'Ana Sayfaya Git'
|
||||
}
|
||||
};
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error page for the Dashboard Layout
|
||||
* This component will be automatically used by Next.js when an error occurs
|
||||
* in any page within the (DashboardLayout) group
|
||||
*/
|
||||
export default function Error({ error, reset }: ErrorPageProps) {
|
||||
// Get the language from URL params or default to English
|
||||
const searchParams = useSearchParams();
|
||||
const [lang, setLang] = useState<'en' | 'tr'>((searchParams?.get('lang') as 'en' | 'tr') || 'en');
|
||||
|
||||
// Ensure lang is valid
|
||||
const validLang = lang === 'tr' ? 'tr' : 'en';
|
||||
const t = translations[validLang];
|
||||
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error('Dashboard error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
<Card className="w-full max-w-md shadow-lg">
|
||||
<CardHeader className="bg-red-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" />
|
||||
<CardTitle className="text-red-700">{t.errorTitle}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-700">
|
||||
{t.errorMessage}
|
||||
</p>
|
||||
|
||||
<div className="p-4 bg-gray-100 rounded-md">
|
||||
<p className="font-mono text-sm break-all">
|
||||
{error.message || t.unknownError}
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="font-mono text-xs text-gray-500 mt-2">
|
||||
{t.errorId}: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex justify-between space-x-4 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t.tryAgain}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex-1 bg-primary"
|
||||
asChild
|
||||
>
|
||||
<Link href="/">
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
{t.goHome}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
WebServices/management-frontend/src/app/error.tsx
Normal file
108
WebServices/management-frontend/src/app/error.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
// Translations for error messages
|
||||
const translations = {
|
||||
en: {
|
||||
errorTitle: 'An error occurred',
|
||||
errorMessage: 'Sorry, something went wrong while loading this page.',
|
||||
unknownError: 'Unknown error',
|
||||
errorId: 'Error ID',
|
||||
tryAgain: 'Try again',
|
||||
goHome: 'Go to Home'
|
||||
},
|
||||
tr: {
|
||||
errorTitle: 'Bir hata oluştu',
|
||||
errorMessage: 'Üzgünüz, bu sayfa yüklenirken bir sorun oluştu.',
|
||||
unknownError: 'Bilinmeyen hata',
|
||||
errorId: 'Hata ID',
|
||||
tryAgain: 'Tekrar dene',
|
||||
goHome: 'Ana Sayfaya Git'
|
||||
}
|
||||
};
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error page for the Dashboard Layout
|
||||
* This component will be automatically used by Next.js when an error occurs
|
||||
* in any page within the (DashboardLayout) group
|
||||
*/
|
||||
export default function Error({ error, reset }: ErrorPageProps) {
|
||||
// Get the language from URL params or default to English
|
||||
const searchParams = useSearchParams();
|
||||
const [lang, setLang] = useState<'en' | 'tr'>((searchParams?.get('lang') as 'en' | 'tr') || 'en');
|
||||
|
||||
// Ensure lang is valid
|
||||
const validLang = lang === 'tr' ? 'tr' : 'en';
|
||||
const t = translations[validLang];
|
||||
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error('Dashboard error:', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
||||
<Card className="w-full max-w-md shadow-lg">
|
||||
<CardHeader className="bg-red-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" />
|
||||
<CardTitle className="text-red-700">{t.errorTitle}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-700">
|
||||
{t.errorMessage}
|
||||
</p>
|
||||
|
||||
<div className="p-4 bg-gray-100 rounded-md">
|
||||
<p className="font-mono text-sm break-all">
|
||||
{error.message || t.unknownError}
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="font-mono text-xs text-gray-500 mt-2">
|
||||
{t.errorId}: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex justify-between space-x-4 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t.tryAgain}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex-1 bg-primary"
|
||||
asChild
|
||||
>
|
||||
<Link href="/">
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
{t.goHome}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { CreateComponentProps } from "./types";
|
||||
import { CreateComponentProps, FieldDefinition } from "./types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
// Import field definitions type
|
||||
interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function CreateComponent<T>({
|
||||
refetch,
|
||||
setMode,
|
||||
@@ -140,13 +128,13 @@ export function CreateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
{errorMessage && (
|
||||
@@ -159,13 +147,13 @@ export function CreateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
rows={3}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
@@ -179,7 +167,7 @@ export function CreateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Select
|
||||
@@ -188,7 +176,7 @@ export function CreateComponent<T>({
|
||||
disabled={field.readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((option) => (
|
||||
@@ -216,7 +204,7 @@ export function CreateComponent<T>({
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
{errorMessage && (
|
||||
|
||||
@@ -69,13 +69,14 @@ export function FormDisplay<T>({
|
||||
};
|
||||
|
||||
loadSchemaDefinitions();
|
||||
}, [formProps, mode]);
|
||||
}, [formProps, mode, lang]); // Added lang as a dependency to ensure re-fetch when language changes
|
||||
|
||||
// Render the appropriate component based on the mode
|
||||
switch (mode) {
|
||||
case "create":
|
||||
return (
|
||||
<CreateComponent<T>
|
||||
key={`create-${lang}`} // Add key with lang to force re-render on language change
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
setSelectedItem={setSelectedItem}
|
||||
@@ -88,6 +89,7 @@ export function FormDisplay<T>({
|
||||
case "update":
|
||||
return initialData ? (
|
||||
<UpdateComponent<T>
|
||||
key={`update-${lang}`} // Add key with lang to force re-render on language change
|
||||
initialData={initialData}
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
@@ -101,6 +103,7 @@ export function FormDisplay<T>({
|
||||
case "view":
|
||||
return initialData ? (
|
||||
<ViewComponent<T>
|
||||
key={`view-${lang}`} // Add key with lang to force re-render on language change
|
||||
initialData={initialData}
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
@@ -108,7 +111,7 @@ export function FormDisplay<T>({
|
||||
onCancel={onCancel}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
formProps={formProps}
|
||||
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
|
||||
/>
|
||||
) : null;
|
||||
default:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { UpdateComponentProps } from "./types";
|
||||
import { UpdateComponentProps, FieldDefinition } from "./types";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -13,18 +13,6 @@ import { useForm } from "react-hook-form";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
// Import field definitions type
|
||||
interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
name?: string; // Add name property for TypeScript compatibility
|
||||
}
|
||||
|
||||
export function UpdateComponent<T>({
|
||||
initialData,
|
||||
refetch,
|
||||
@@ -154,13 +142,13 @@ export function UpdateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
{errorMessage && (
|
||||
@@ -173,13 +161,13 @@ export function UpdateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={fieldName}
|
||||
{...register(fieldName)}
|
||||
placeholder={t[fieldName] || field.label}
|
||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
rows={3}
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
@@ -193,7 +181,7 @@ export function UpdateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Select
|
||||
@@ -202,7 +190,7 @@ export function UpdateComponent<T>({
|
||||
disabled={field.readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t[fieldName] || field.label} />
|
||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((option) => (
|
||||
@@ -230,7 +218,7 @@ export function UpdateComponent<T>({
|
||||
disabled={field.readOnly}
|
||||
/>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
{errorMessage && (
|
||||
@@ -243,7 +231,7 @@ export function UpdateComponent<T>({
|
||||
return (
|
||||
<div className="space-y-2" key={fieldName}>
|
||||
<Label htmlFor={fieldName}>
|
||||
{t[fieldName] || field.label}
|
||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
|
||||
@@ -2,21 +2,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { ViewComponentProps } from "./types";
|
||||
import { ViewComponentProps, FieldDefinition } from "./types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { z } from "zod";
|
||||
|
||||
// Import field definitions type
|
||||
export interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
name?: string; // Add name property for TypeScript compatibility
|
||||
}
|
||||
|
||||
// Utility function to format field label
|
||||
const formatFieldLabel = (fieldName: string) =>
|
||||
@@ -103,7 +92,7 @@ const ViewFieldGroup: React.FC<{
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={value}
|
||||
label={field.label}
|
||||
label={field.label[lang as "en" | "tr"]}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
hasError={hasError}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
"use client";
|
||||
|
||||
// Import field definitions type
|
||||
export interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: { tr: string; en: string };
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Define the FormMode type to ensure consistency
|
||||
export type FormMode = "list" | "create" | "update" | "view";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching";
|
||||
import { useDataFetching, ApiResponse } from "./useDataFetching";
|
||||
import { RequestParams } from "../schemas";
|
||||
|
||||
/**
|
||||
* Hook for fetching data from Next.js API routes
|
||||
@@ -11,35 +12,37 @@ export function useApiData<T>(
|
||||
initialParams: Partial<RequestParams> = {}
|
||||
) {
|
||||
// Define the fetch function that will be passed to useDataFetching
|
||||
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => {
|
||||
const fetchFromApi = async (
|
||||
params: RequestParams
|
||||
): Promise<ApiResponse<T>> => {
|
||||
try {
|
||||
// Prepare the request body with action and all params
|
||||
const requestBody = {
|
||||
action: 'list',
|
||||
action: "list",
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
orderField: params.orderField,
|
||||
orderType: params.orderType,
|
||||
query: params.query
|
||||
query: params.query,
|
||||
};
|
||||
|
||||
|
||||
// Make the API request using POST
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
|
||||
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: [],
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import { PageProps } from "@/validations/translations/translation";
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
export interface DashboardPageParams {
|
||||
/**
|
||||
* The active page path, e.g., "/application", "/dashboard"
|
||||
*/
|
||||
pageUrl: string;
|
||||
|
||||
/**
|
||||
* The search parameters from Next.js
|
||||
*/
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}
|
||||
|
||||
export interface DashboardPageResult {
|
||||
/**
|
||||
* The active page path
|
||||
*/
|
||||
activePage: string;
|
||||
|
||||
/**
|
||||
* The resolved search parameters
|
||||
*/
|
||||
searchParamsInstance: { [key: string]: string | undefined };
|
||||
|
||||
/**
|
||||
* The current language, either from search params or default
|
||||
*/
|
||||
lang: "en" | "tr";
|
||||
|
||||
/**
|
||||
* The page component to render
|
||||
*/
|
||||
PageComponent: React.FC<PageProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve and prepare dashboard page data
|
||||
* Throws errors for Next.js error boundary to catch
|
||||
*
|
||||
* @param params The dashboard page parameters
|
||||
* @returns The processed dashboard page data
|
||||
* @throws Error if page URL is invalid or page component is not found
|
||||
*/
|
||||
export async function useDashboardPage({
|
||||
pageUrl,
|
||||
searchParams,
|
||||
}: DashboardPageParams): Promise<DashboardPageResult> {
|
||||
let searchParamsInstance: { [key: string]: string | undefined } = {};
|
||||
const defaultLang = "en";
|
||||
// Validate pageUrl
|
||||
if (!pageUrl || typeof pageUrl !== "string") {
|
||||
throw new Error(`Invalid page URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
// Resolve search params
|
||||
try {
|
||||
searchParamsInstance = await searchParams;
|
||||
} catch (err) {
|
||||
console.error("Error resolving search parameters:", err);
|
||||
// Still throw the error to be caught by Next.js error boundary
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Determine language
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
|
||||
|
||||
// Validate language
|
||||
if (lang !== "en" && lang !== "tr") {
|
||||
console.warn(
|
||||
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
|
||||
);
|
||||
}
|
||||
|
||||
// Get page component
|
||||
const PageComponent = retrievePageByUrl(pageUrl);
|
||||
|
||||
// Check if page component exists
|
||||
if (!PageComponent) {
|
||||
throw new Error(`Page component not found for URL: ${pageUrl}`);
|
||||
}
|
||||
|
||||
return {
|
||||
activePage: pageUrl,
|
||||
searchParamsInstance,
|
||||
lang,
|
||||
PageComponent,
|
||||
};
|
||||
}
|
||||
|
||||
export default useDashboardPage;
|
||||
@@ -1,22 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
|
||||
export interface RequestParams {
|
||||
page: number;
|
||||
size: number;
|
||||
orderField: string[];
|
||||
orderType: string[];
|
||||
query: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ResponseMetadata {
|
||||
totalCount: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
pageCount: number;
|
||||
allCount?: number;
|
||||
next: boolean;
|
||||
back: boolean;
|
||||
}
|
||||
import { RequestParams, ResponseMetadata } from "../schemas";
|
||||
|
||||
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
||||
|
||||
@@ -104,19 +87,21 @@ export function useDataFetching<T>(
|
||||
|
||||
// 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);
|
||||
|
||||
const paramsChanged =
|
||||
JSON.stringify(prevRequestParamsRef.current) !==
|
||||
JSON.stringify(requestParams);
|
||||
|
||||
if (initialMountRef.current || paramsChanged) {
|
||||
const timer = setTimeout(() => {
|
||||
fetchDataFromApi();
|
||||
initialMountRef.current = false;
|
||||
prevRequestParamsRef.current = {...requestParams};
|
||||
prevRequestParamsRef.current = { ...requestParams };
|
||||
}, 300); // Debounce
|
||||
|
||||
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;
|
||||
271
WebServices/management-frontend/src/components/layouts/README.md
Normal file
271
WebServices/management-frontend/src/components/layouts/README.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Modular Dashboard Layout System
|
||||
|
||||
This directory contains reusable layout components for building dashboard pages with a consistent structure and appearance.
|
||||
|
||||
## Components
|
||||
|
||||
### DashboardLayout
|
||||
|
||||
The `DashboardLayout` component provides the overall page structure with:
|
||||
- Sidebar navigation
|
||||
- Header
|
||||
- Main content area
|
||||
|
||||
### PageTemplate
|
||||
|
||||
The `PageTemplate` component provides a standardized structure for page content with:
|
||||
- Header section with title and selection components
|
||||
- Search filters section
|
||||
- Action buttons section
|
||||
- Pagination tools section
|
||||
- Content display section
|
||||
- Form display for create/update/view modes
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's an example of how to use these components to create a new dashboard page:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { useApiData } from "@/components/common/hooks/useApiData";
|
||||
import { CardDisplay } from "@/components/common/CardDisplay";
|
||||
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
||||
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||
import { FormMode } from "@/components/common/FormDisplay/types";
|
||||
import { PageTemplate } from "@/components/layouts/PageTemplate";
|
||||
import type { GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||
|
||||
// Import your schema and translations
|
||||
import * as schema from "./schema";
|
||||
import { translations } from "./language";
|
||||
|
||||
const ExamplePage: React.FC<{ lang: "en" | "tr", queryParams: any }> = ({
|
||||
lang = "en",
|
||||
queryParams
|
||||
}) => {
|
||||
// API data hook
|
||||
const {
|
||||
data,
|
||||
pagination,
|
||||
loading,
|
||||
error,
|
||||
updatePagination,
|
||||
refetch
|
||||
} = useApiData<schema.ExampleData>('/api/examples');
|
||||
|
||||
// State management
|
||||
const [mode, setMode] = useState<FormMode>("list");
|
||||
const [selectedItem, setSelectedItem] = useState<schema.ExampleData | null>(null);
|
||||
const [gridCols, setGridCols] = useState<GridSize>(3);
|
||||
const [currentLang, setCurrentLang] = useState<Language>(lang as Language);
|
||||
|
||||
// Fields to display in cards
|
||||
const showFields = ["name", "type", "status"];
|
||||
|
||||
// Query handling
|
||||
const handleQueryChange = (key: string, value: string | null) => {
|
||||
const newQuery = { ...pagination.query };
|
||||
|
||||
if (value === null || value.trim() === "") {
|
||||
delete newQuery[key];
|
||||
} else {
|
||||
newQuery[key] = value;
|
||||
}
|
||||
|
||||
updatePagination({
|
||||
page: 1,
|
||||
query: newQuery,
|
||||
});
|
||||
};
|
||||
|
||||
// Reset all filters
|
||||
const handleResetAllFilters = () => {
|
||||
updatePagination({
|
||||
page: 1,
|
||||
query: {},
|
||||
});
|
||||
};
|
||||
|
||||
// Action handlers
|
||||
const handleCardClick = (item: schema.ExampleData) => {
|
||||
console.log("Card clicked:", item);
|
||||
};
|
||||
|
||||
const handleViewClick = (item: schema.ExampleData) => {
|
||||
setSelectedItem(item);
|
||||
setMode("view");
|
||||
};
|
||||
|
||||
const handleUpdateClick = (item: schema.ExampleData) => {
|
||||
setSelectedItem(item);
|
||||
setMode("update");
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
setSelectedItem(null);
|
||||
setMode("create");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setMode("list");
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
// Search section component
|
||||
const SearchSection = (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex space-x-4">
|
||||
{/* Type selector */}
|
||||
<TypeQueryModifier
|
||||
fieldKey="type"
|
||||
value={pagination.query["type"] || ""}
|
||||
options={[
|
||||
{ value: "type1", label: translations[lang].type1 },
|
||||
{ value: "type2", label: translations[lang].type2 }
|
||||
]}
|
||||
onQueryChange={handleQueryChange}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
{/* Text search */}
|
||||
<TextQueryModifier
|
||||
fieldKey="name"
|
||||
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
||||
label={translations[lang].search || "Search"}
|
||||
onQueryChange={handleQueryChange}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
{/* Status dropdown */}
|
||||
<SelectQueryModifier
|
||||
fieldKey="status"
|
||||
value={pagination.query["status"] || ""}
|
||||
label={translations[lang].status || "Status"}
|
||||
options={[
|
||||
{ value: "active", label: translations[lang].active || "Active" },
|
||||
{ value: "inactive", label: translations[lang].inactive || "Inactive" }
|
||||
]}
|
||||
onQueryChange={handleQueryChange}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reset filters button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleResetAllFilters}
|
||||
className="px-4 py-2 bg-gray-100 rounded-md hover:bg-gray-200"
|
||||
>
|
||||
{translations[lang].resetAll || "Reset All"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Form component
|
||||
const FormComponent = selectedItem || mode === "create" ? (
|
||||
<FormDisplay<schema.ExampleData>
|
||||
initialData={selectedItem || undefined}
|
||||
mode={mode}
|
||||
refetch={refetch}
|
||||
setMode={setMode}
|
||||
setSelectedItem={setSelectedItem}
|
||||
onCancel={handleCancel}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
formProps={{
|
||||
fieldDefinitions: mode === 'create' ? schema.createFieldDefinitions :
|
||||
mode === 'update' ? schema.updateFieldDefinitions :
|
||||
schema.viewFieldDefinitions,
|
||||
validationSchema: mode === 'create' ? schema.CreateExampleSchema :
|
||||
mode === 'update' ? schema.UpdateExampleSchema :
|
||||
schema.ViewExampleSchema,
|
||||
fieldsByMode: schema.fieldsByMode
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// Content display component
|
||||
const ContentDisplay = (
|
||||
<CardDisplay
|
||||
showFields={showFields}
|
||||
data={data}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
error={error}
|
||||
loading={loading}
|
||||
titleField="name"
|
||||
onCardClick={handleCardClick}
|
||||
gridCols={gridCols}
|
||||
showViewIcon={true}
|
||||
showUpdateIcon={true}
|
||||
onViewClick={handleViewClick}
|
||||
onUpdateClick={handleUpdateClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
title={translations[lang].examplePageTitle || "Example Page"}
|
||||
lang={lang}
|
||||
translations={translations}
|
||||
searchSection={SearchSection}
|
||||
data={data}
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
loading={loading}
|
||||
error={error}
|
||||
refetch={refetch}
|
||||
contentDisplay={ContentDisplay}
|
||||
formComponent={FormComponent}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
handleCreateClick={handleCreateClick}
|
||||
handleCancel={handleCancel}
|
||||
setLang={setCurrentLang}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExamplePage;
|
||||
```
|
||||
|
||||
## Integration with Next.js App Router
|
||||
|
||||
To use these components with Next.js App Router, update your page component like this:
|
||||
|
||||
```tsx
|
||||
// src/app/(DashboardLayout)/your-page/page.tsx
|
||||
import React from "react";
|
||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||
import DashboardLayout from "@/components/layouts/DashboardLayout";
|
||||
|
||||
async function YourPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
}) {
|
||||
const activePage = "/your-page";
|
||||
const searchParamsInstance = await searchParams;
|
||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
|
||||
const PageComponent = retrievePageByUrl(activePage);
|
||||
|
||||
return (
|
||||
<DashboardLayout lang={lang} activePage={activePage}>
|
||||
<PageComponent lang={lang} queryParams={searchParamsInstance} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default YourPage;
|
||||
```
|
||||
|
||||
This modular approach makes it easy to create new dashboard pages with consistent structure and behavior.
|
||||
@@ -16,7 +16,10 @@ import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSele
|
||||
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||
import type { FormMode } from "@/components/common/FormDisplay/types";
|
||||
|
||||
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||
const ApplicationPage: React.FC<PageProps> = ({ lang: initialLang = "en" }) => {
|
||||
// Add local state for language to ensure it persists when changed
|
||||
const [lang, setLang] = useState<Language>(initialLang as Language);
|
||||
|
||||
// Use the API data hook directly
|
||||
const {
|
||||
data,
|
||||
@@ -128,23 +131,22 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="container mx-auto p-4 overflow-y-auto" >
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<GridSelectionComponent
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
<div className="flex space-x-4">
|
||||
{/* Grid Selection */}
|
||||
<GridSelectionComponent
|
||||
gridCols={gridCols}
|
||||
setGridCols={setGridCols}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
@@ -174,14 +176,14 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
{translations[lang].filterSelection || "Filter Selection"}
|
||||
</div>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleResetAllFilters}
|
||||
className="text-xs"
|
||||
>
|
||||
{translations[lang].resetAll || "Reset All"}
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { flattenFieldDefinitions } from "../schemas/zodSchemas";
|
||||
|
||||
export interface ApplicationData {
|
||||
id?: number;
|
||||
name: string;
|
||||
application_code: string;
|
||||
site_url: string;
|
||||
application_type: string;
|
||||
application_for?: string;
|
||||
description?: string;
|
||||
active: boolean;
|
||||
deleted?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// Base schema with all possible fields
|
||||
const ApplicationBaseSchema = z.object({
|
||||
@@ -22,6 +37,34 @@ const ApplicationBaseSchema = z.object({
|
||||
updated_at: z.string().optional(),
|
||||
});
|
||||
|
||||
const ApplicationBaseTranslationTr = {
|
||||
uu_id: "UUID",
|
||||
name: "Name",
|
||||
application_code: "Application Code",
|
||||
site_url: "Site URL",
|
||||
application_type: "Application Type",
|
||||
application_for: "Application For",
|
||||
description: "Description",
|
||||
active: "Active",
|
||||
deleted: "Deleted",
|
||||
created_at: "Created At",
|
||||
updated_at: "Updated At",
|
||||
};
|
||||
|
||||
const ApplicationBaseTranslationEn = {
|
||||
uu_id: "UUID",
|
||||
name: "Name",
|
||||
application_code: "Application Code",
|
||||
site_url: "Site URL",
|
||||
application_type: "Application Type",
|
||||
application_for: "Application For",
|
||||
description: "Description",
|
||||
active: "Active",
|
||||
deleted: "Deleted",
|
||||
created_at: "Created At",
|
||||
updated_at: "Updated At",
|
||||
};
|
||||
|
||||
// Schema for creating a new application
|
||||
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
||||
uu_id: true,
|
||||
@@ -50,31 +93,6 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
||||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||
|
||||
// Define field definition type
|
||||
export interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export interface ApplicationData {
|
||||
id?: number;
|
||||
name: string;
|
||||
application_code: string;
|
||||
site_url: string;
|
||||
application_type: string;
|
||||
application_for?: string;
|
||||
description?: string;
|
||||
active: boolean;
|
||||
deleted?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// Base field definitions grouped by section
|
||||
const baseFieldDefinitions = {
|
||||
// Identification fields
|
||||
@@ -82,11 +100,30 @@ const baseFieldDefinitions = {
|
||||
title: "Identification Information",
|
||||
order: 1,
|
||||
fields: {
|
||||
uu_id: { type: "text", label: "UUID", readOnly: true, required: false },
|
||||
name: { type: "text", label: "Name", readOnly: false, required: true },
|
||||
uu_id: {
|
||||
type: "text",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.uu_id,
|
||||
en: ApplicationBaseTranslationEn.uu_id,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
},
|
||||
name: {
|
||||
type: "text",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.name,
|
||||
en: ApplicationBaseTranslationEn.name,
|
||||
},
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
application_code: {
|
||||
type: "text",
|
||||
label: "Application Code",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_code,
|
||||
en: ApplicationBaseTranslationEn.application_code,
|
||||
},
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
@@ -100,27 +137,39 @@ const baseFieldDefinitions = {
|
||||
fields: {
|
||||
site_url: {
|
||||
type: "text",
|
||||
label: "Site URL",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.site_url,
|
||||
en: ApplicationBaseTranslationEn.site_url,
|
||||
},
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
application_type: {
|
||||
type: "select",
|
||||
label: "Application Type",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_type,
|
||||
en: ApplicationBaseTranslationEn.application_type,
|
||||
},
|
||||
options: ["info", "Dash", "Admin"],
|
||||
readOnly: false,
|
||||
required: true,
|
||||
},
|
||||
application_for: {
|
||||
type: "select",
|
||||
label: "Application For",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.application_for,
|
||||
en: ApplicationBaseTranslationEn.application_for,
|
||||
},
|
||||
options: ["EMP", "OCC"],
|
||||
readOnly: false,
|
||||
required: false,
|
||||
},
|
||||
description: {
|
||||
type: "textarea",
|
||||
label: "Description",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.description,
|
||||
en: ApplicationBaseTranslationEn.description,
|
||||
},
|
||||
readOnly: false,
|
||||
required: false,
|
||||
},
|
||||
@@ -134,14 +183,20 @@ const baseFieldDefinitions = {
|
||||
fields: {
|
||||
active: {
|
||||
type: "checkbox",
|
||||
label: "Active",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.active,
|
||||
en: ApplicationBaseTranslationEn.active,
|
||||
},
|
||||
readOnly: false,
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
deleted: {
|
||||
type: "checkbox",
|
||||
label: "Deleted",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.deleted,
|
||||
en: ApplicationBaseTranslationEn.deleted,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
@@ -156,13 +211,19 @@ const baseFieldDefinitions = {
|
||||
fields: {
|
||||
created_at: {
|
||||
type: "date",
|
||||
label: "Created At",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.created_at,
|
||||
en: ApplicationBaseTranslationEn.created_at,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
},
|
||||
updated_at: {
|
||||
type: "date",
|
||||
label: "Updated At",
|
||||
label: {
|
||||
tr: ApplicationBaseTranslationTr.updated_at,
|
||||
en: ApplicationBaseTranslationEn.updated_at,
|
||||
},
|
||||
readOnly: true,
|
||||
required: false,
|
||||
},
|
||||
@@ -170,28 +231,6 @@ const baseFieldDefinitions = {
|
||||
},
|
||||
};
|
||||
|
||||
// Helper function to flatten grouped field definitions into a flat structure
|
||||
const flattenFieldDefinitions = (
|
||||
groupedDefs: any
|
||||
): Record<string, FieldDefinition> => {
|
||||
const result: Record<string, FieldDefinition> = {};
|
||||
|
||||
Object.entries(groupedDefs).forEach(
|
||||
([groupName, groupConfig]: [string, any]) => {
|
||||
Object.entries(groupConfig.fields).forEach(
|
||||
([fieldName, fieldConfig]: [string, any]) => {
|
||||
result[fieldName] = {
|
||||
...fieldConfig,
|
||||
group: groupName,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Create a flat version of the field definitions for compatibility
|
||||
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||
|
||||
@@ -386,6 +425,3 @@ export const fieldsByMode = {
|
||||
update: Object.keys(updateFieldDefinitions),
|
||||
view: Object.keys(viewFieldDefinitions),
|
||||
};
|
||||
|
||||
// Note: Direct fetch function has been removed to use the API route instead
|
||||
// Data fetching is now handled in the hooks.ts file using the POST endpoint
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Define field definition type
|
||||
interface FieldDefinition {
|
||||
type: string;
|
||||
group: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
// Helper function to flatten grouped field definitions into a flat structure
|
||||
const flattenFieldDefinitions = (
|
||||
groupedDefs: any
|
||||
): Record<string, FieldDefinition> => {
|
||||
const result: Record<string, FieldDefinition> = {};
|
||||
|
||||
Object.entries(groupedDefs).forEach(
|
||||
([groupName, groupConfig]: [string, any]) => {
|
||||
Object.entries(groupConfig.fields).forEach(
|
||||
([fieldName, fieldConfig]: [string, any]) => {
|
||||
result[fieldName] = {
|
||||
...fieldConfig,
|
||||
group: groupName,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export type { FieldDefinition };
|
||||
export { flattenFieldDefinitions };
|
||||
Reference in New Issue
Block a user