updated lang change and FormDisplay Components

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

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect } from "react";
import { 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 && (

View File

@@ -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:

View File

@@ -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

View File

@@ -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}

View File

@@ -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";

View File

@@ -1,4 +1,5 @@
import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching";
import { useDataFetching, ApiResponse } from "./useDataFetching";
import { RequestParams } from "../schemas";
/**
* Hook for fetching data from Next.js API routes
@@ -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: [],

View File

@@ -0,0 +1,93 @@
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
import { PageProps } from "@/validations/translations/translation";
import React, { ReactElement } from "react";
export interface DashboardPageParams {
/**
* The active page path, e.g., "/application", "/dashboard"
*/
pageUrl: string;
/**
* The search parameters from Next.js
*/
searchParams: Promise<{ [key: string]: string | undefined }>;
}
export interface DashboardPageResult {
/**
* The active page path
*/
activePage: string;
/**
* The resolved search parameters
*/
searchParamsInstance: { [key: string]: string | undefined };
/**
* The current language, either from search params or default
*/
lang: "en" | "tr";
/**
* The page component to render
*/
PageComponent: React.FC<PageProps>;
}
/**
* Hook to retrieve and prepare dashboard page data
* Throws errors for Next.js error boundary to catch
*
* @param params The dashboard page parameters
* @returns The processed dashboard page data
* @throws Error if page URL is invalid or page component is not found
*/
export async function useDashboardPage({
pageUrl,
searchParams,
}: DashboardPageParams): Promise<DashboardPageResult> {
let searchParamsInstance: { [key: string]: string | undefined } = {};
const defaultLang = "en";
// Validate pageUrl
if (!pageUrl || typeof pageUrl !== "string") {
throw new Error(`Invalid page URL: ${pageUrl}`);
}
// Resolve search params
try {
searchParamsInstance = await searchParams;
} catch (err) {
console.error("Error resolving search parameters:", err);
// Still throw the error to be caught by Next.js error boundary
throw err;
}
// Determine language
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
// Validate language
if (lang !== "en" && lang !== "tr") {
console.warn(
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
);
}
// Get page component
const PageComponent = retrievePageByUrl(pageUrl);
// Check if page component exists
if (!PageComponent) {
throw new Error(`Page component not found for URL: ${pageUrl}`);
}
return {
activePage: pageUrl,
searchParamsInstance,
lang,
PageComponent,
};
}
export default useDashboardPage;

View File

@@ -1,22 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
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);

View File

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

View File

@@ -0,0 +1,72 @@
"use client";
import React, { ReactNode } from "react";
import Header from "@/components/header/Header";
import ClientMenu from "@/components/menu/menu";
interface DashboardLayoutProps {
children: ReactNode;
lang: "en" | "tr";
activePage: string;
// Optional props for client-frontend application
sidebarContent?: ReactNode;
customHeader?: ReactNode;
pageInfo?: Record<string, string>;
searchPlaceholder?: Record<string, string>;
}
/**
* A reusable dashboard layout component that provides consistent structure
* for all dashboard pages with sidebar, header, and content area.
*/
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
lang,
activePage,
sidebarContent,
customHeader,
pageInfo,
searchPlaceholder,
}) => {
return (
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-y-auto">
{/* Sidebar */}
<aside className="w-1/4 border-r p-4 overflow-y-auto">
{sidebarContent ? (
sidebarContent
) : (
<ClientMenu lang={lang} activePage={activePage} />
)}
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4 overflow-y-auto">
{/* Header Component - Either custom or default */}
{customHeader ? (
customHeader
) : pageInfo && searchPlaceholder ? (
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
<h1 className="text-2xl font-semibold">{pageInfo[lang]}</h1>
<div className="flex items-center space-x-4">
<input
type="text"
placeholder={searchPlaceholder[lang]}
className="border px-3 py-2 rounded-lg"
/>
<div className="w-10 h-10 bg-gray-300 rounded-full"></div>
</div>
</header>
) : (
<Header lang={lang} />
)}
{/* Page Content */}
<div className={`${customHeader ? 'p-4 overflow-y-auto' : 'container mx-auto p-4'}`}>
{children}
</div>
</div>
</div>
);
};
export default DashboardLayout;

View File

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

View File

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

View File

@@ -16,7 +16,10 @@ import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSele
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
import 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 */}

View File

@@ -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

View File

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