Card Component implemented

This commit is contained in:
2025-04-29 20:44:39 +03:00
parent 113e43c7d7
commit 0052c92974
26 changed files with 2273 additions and 65 deletions

View File

@@ -27,7 +27,7 @@ export const SearchComponent: React.FC<SearchComponentProps> = ({
// Handle selection button click
const handleTypeSelect = (type: "employee" | "occupant") => {
setSelectedType(type);
// Include type in search query
handleSearch(searchQuery, selectedUrl, type);
};
@@ -35,13 +35,15 @@ export const SearchComponent: React.FC<SearchComponentProps> = ({
// Handle search with all parameters
const handleSearch = (query: string, url: string, type: "employee" | "occupant") => {
const searchParams: Record<string, string> = {};
if (url) {
searchParams.site_url = url;
}
searchParams.name = query
if (query) {
searchParams.name = query
}
searchParams.application_for = type === "employee" ? "EMP" : "OCC";
// Call onSearch with the search parameters
// The parent component will handle resetting pagination
onSearch(searchParams);
@@ -102,12 +104,12 @@ export const SearchComponent: React.FC<SearchComponentProps> = ({
className="pl-8 w-full h-10"
/>
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Button
variant="default"
size="sm"
<Button
variant="default"
size="sm"
className="ml-2"
onClick={() => {
handleSearch(searchQuery, selectedUrl, selectedType);
handleSearch(searchQuery, selectedUrl, selectedType);
}}
>
<Search className="h-4 w-4" />
@@ -121,8 +123,8 @@ export const SearchComponent: React.FC<SearchComponentProps> = ({
{translations.siteUrl && translations.siteUrl[lang as keyof LanguageTranslation]}
</label>
<div className="w-full">
<Select
value={selectedUrl}
<Select
value={selectedUrl}
onValueChange={(value) => {
setSelectedUrl(value);
handleSearch(searchQuery, value, selectedType);

View File

@@ -0,0 +1,49 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
interface ActionButtonsComponentProps {
onCreateClick: () => void;
translations: Record<string, any>;
lang: string;
customButtons?: {
label: string;
onClick: () => void;
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
icon?: React.ReactNode;
}[];
}
export const ActionButtonsComponent: React.FC<ActionButtonsComponentProps> = ({
onCreateClick,
translations,
lang,
customButtons = [],
}) => {
const t = translations[lang] || {};
return (
<div className="flex justify-between items-center my-4">
<div className="flex space-x-2">
<Button onClick={onCreateClick} className="flex items-center">
<Plus className="mr-2 h-4 w-4" />
{t.create || "Create"}
</Button>
{/* Render custom buttons */}
{customButtons.map((button, index) => (
<Button
key={index}
onClick={button.onClick}
variant={button.variant || "default"}
className="flex items-center"
>
{button.icon && <span className="mr-2">{button.icon}</span>}
{button.label}
</Button>
))}
</div>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
"use client";
import React from "react";
interface FormDisplayProps<T> {
initialData?: T;
mode: "list" | "create" | "update";
refetch?: () => void;
setMode: React.Dispatch<React.SetStateAction<"list" | "create" | "update">>;
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
onCancel: () => void;
lang: string;
FormComponent: React.FC<any>;
formProps?: Record<string, any>;
}
export function FormDisplay<T>({
initialData,
mode,
refetch,
setMode,
setSelectedItem,
onCancel,
lang,
FormComponent,
formProps = {},
}: FormDisplayProps<T>) {
return (
<FormComponent
initialData={initialData}
mode={mode}
refetch={refetch}
setMode={setMode}
setSelectedItem={setSelectedItem}
onCancel={onCancel}
lang={lang}
{...formProps}
/>
);
}

View File

@@ -0,0 +1,199 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PagePagination } from "./hooks/useDataFetching";
interface PaginationToolsComponentProps {
pagination: PagePagination;
updatePagination: (updates: Partial<PagePagination>) => void;
loading: boolean;
lang: string;
translations: Record<string, any>;
}
export const PaginationToolsComponent: React.FC<PaginationToolsComponentProps> = ({
pagination,
updatePagination,
loading,
lang,
translations
}) => {
const t = translations[lang] || {};
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= pagination.totalPages) {
updatePagination({ page: newPage });
}
};
return (
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
{/* Pagination stats - left side */}
<div className="text-sm text-muted-foreground">
<div>
{t.showing || "Showing"}{" "}
{/* Show the range based on filtered count when available */}
{(pagination.totalCount || pagination.allCount || 0) > 0
? (pagination.page - 1) * pagination.size + 1
: 0}{" "}
-{" "}
{Math.min(
pagination.page * pagination.size,
pagination.totalCount || pagination.allCount || 0
)}{" "}
{t.of || "of"} {pagination.totalCount || pagination.allCount || 0} {t.items || "items"}
</div>
{pagination.totalCount &&
pagination.totalCount !== (pagination.allCount || 0) && (
<div>
{t.total || "Total"}: {pagination.allCount || 0} {t.items || "items"} ({t.filtered || "Filtered"}:{" "}
{pagination.totalCount} {t.items || "items"})
</div>
)}
</div>
{/* Navigation buttons - center */}
<div className="flex items-center space-x-2">
{
pagination.back ? (
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(pagination.page - 1)}
>
{t.previous || "Previous"}
</Button>
) : (
<Button
variant="ghost"
size="sm"
disabled
>
{t.previous || "Previous"}
</Button>
)
}
{/* Page number buttons */}
<div className="flex items-center space-x-1">
{Array.from(
{
length: Math.min(
5,
Math.max(
1,
Math.ceil(
(pagination.totalCount &&
pagination.totalCount !== pagination.allCount
? pagination.totalCount
: pagination.allCount || 0) / pagination.size
)
)
),
},
(_, i) => {
// Show pages around current page
let pageNum;
const calculatedTotalPages = Math.max(
1,
Math.ceil(
(pagination.totalCount &&
pagination.totalCount !== pagination.allCount
? pagination.totalCount
: pagination.allCount || 0) / pagination.size
)
);
if (calculatedTotalPages <= 5) {
pageNum = i + 1;
} else if (pagination.page <= 3) {
pageNum = i + 1;
} else if (pagination.page >= calculatedTotalPages - 2) {
pageNum = calculatedTotalPages - 4 + i;
} else {
pageNum = pagination.page - 2 + i;
}
return (
<Button
key={pageNum}
variant={pagination.page === pageNum ? "default" : "outline"}
size="sm"
className="w-9 h-9 p-0"
onClick={() => handlePageChange(pageNum)}
disabled={loading}
>
{pageNum}
</Button>
);
}
)}
</div>
{
pagination.page < pagination.totalPages ? (
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(pagination.page + 1)}
>
{t.next || "Next"}
</Button>
) : (
<Button
variant="ghost"
size="sm"
disabled
>
{t.next || "Next"}
</Button>
)
}
{/* Page text display */}
<span className="px-4 py-1 text-sm text-muted-foreground">
{t.page || "Page"} {pagination.page} {t.of || "of"}{" "}
{Math.max(
1,
Math.ceil(
(pagination.totalCount &&
pagination.totalCount !== pagination.allCount
? pagination.totalCount
: pagination.allCount || 0) / pagination.size
)
)}
</span>
</div>
{/* Items per page selector - right side */}
<div className="flex items-center space-x-2">
<span className="text-sm text-muted-foreground">{t.itemsPerPage || "Items per page"}</span>
<Select
value={pagination.size.toString()}
onValueChange={(value) => {
updatePagination({
size: Number(value),
page: 1, // Reset to first page when changing page size
});
}}
>
<SelectTrigger className="w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
};

View File

@@ -0,0 +1,221 @@
"use client";
import React, { useState } from "react";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { User, Building, Filter, Search, Link } from "lucide-react";
interface SearchComponentProps {
onSearch: (query: Record<string, string>) => void;
translations: Record<string, any>;
lang: string;
typeOptions?: {
value: string;
label: string;
icon?: React.ReactNode;
}[];
urlOptions?: string[];
additionalFields?: {
name: string;
label: string;
type: "text" | "select";
options?: { value: string; label: string }[];
}[];
}
export const SearchComponent: React.FC<SearchComponentProps> = ({
onSearch,
translations,
lang,
typeOptions = [],
urlOptions = [],
additionalFields = [],
}) => {
const [selectedType, setSelectedType] = useState<string>(typeOptions.length > 0 ? typeOptions[0].value : "");
const [selectedUrl, setSelectedUrl] = useState<string>("");
const [searchQuery, setSearchQuery] = useState<string>("");
const [additionalValues, setAdditionalValues] = useState<Record<string, string>>({});
// Handle selection button click
const handleTypeSelect = (type: string) => {
setSelectedType(type);
// Include type in search query
handleSearch(searchQuery, selectedUrl, type, additionalValues);
};
// Handle search with all parameters
const handleSearch = (
query: string,
url: string,
type: string,
additionalValues: Record<string, string>
) => {
const searchParams: Record<string, string> = {};
if (url) {
searchParams.site_url = url;
}
if (query) {
searchParams.name = query;
}
if (type) {
searchParams.type = type;
}
// Add additional field values
Object.entries(additionalValues).forEach(([key, value]) => {
if (value) {
searchParams[key] = value;
}
});
// Call onSearch with the search parameters
onSearch(searchParams);
};
const handleAdditionalFieldChange = (fieldName: string, value: string) => {
setAdditionalValues((prev) => ({
...prev,
[fieldName]: value,
}));
};
const t = translations[lang] || {};
return (
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row">
{/* Type selection - vertical on the left (w-1/2) */}
{typeOptions.length > 0 && (
<div className="w-full md:w-1/2 flex flex-col space-y-4 md:pr-4 mb-4 md:mb-0">
<div className="font-medium text-sm mb-2 flex items-center">
<User className="mr-2 h-4 w-4" />
{t.typeSelection || "Type Selection"}
</div>
{typeOptions.map((option) => (
<Button
key={option.value}
variant={selectedType === option.value ? "default" : "outline"}
size="lg"
onClick={() => handleTypeSelect(option.value)}
className="w-full h-14 mb-2"
>
{option.icon || <User className="mr-2 h-4 w-4" />}
{option.label}
</Button>
))}
</div>
)}
{/* Filters on the right (w-1/2) */}
<div className={`w-full ${typeOptions.length > 0 ? 'md:w-1/2 md:pl-4' : ''} flex flex-col space-y-4`}>
<div className="font-medium text-sm mb-2 flex items-center">
<Filter className="mr-2 h-4 w-4" />
{t.filterSelection || "Filter Selection"}
</div>
{/* Search input */}
<div className="w-full">
<label className="block text-xs font-medium mb-1">
{t.search || "Search"}
</label>
<div className="relative w-full flex">
<Input
placeholder={`${t.search || "Search"}...`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyUp={(e) => {
if (e.key === 'Enter') {
handleSearch(searchQuery, selectedUrl, selectedType, additionalValues);
}
}}
className="pl-8 w-full h-10"
/>
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Button
variant="default"
size="sm"
className="ml-2"
onClick={() => {
handleSearch(searchQuery, selectedUrl, selectedType, additionalValues);
}}
>
<Search className="h-4 w-4" />
</Button>
</div>
</div>
{/* Site URL dropdown */}
{urlOptions.length > 0 && (
<div className="w-full">
<label className="block text-xs font-medium mb-1">
{t.siteUrl || "Site URL"}
</label>
<div className="w-full">
<Select
value={selectedUrl}
onValueChange={(value) => {
setSelectedUrl(value);
handleSearch(searchQuery, value, selectedType, additionalValues);
}}
>
<SelectTrigger className="w-full h-10">
<SelectValue
placeholder={`${t.siteUrl || "Site URL"}...`}
/>
</SelectTrigger>
<SelectContent>
{urlOptions.map((url) => (
<SelectItem key={url} value={url}>
<div className="flex items-center">
<Link className="mr-2 h-3 w-3" />
{url}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
{/* Additional fields */}
{additionalFields.map((field) => (
<div key={field.name} className="w-full">
<label className="block text-xs font-medium mb-1">
{field.label}
</label>
{field.type === "text" ? (
<Input
value={additionalValues[field.name] || ""}
onChange={(e) => handleAdditionalFieldChange(field.name, e.target.value)}
className="w-full h-10"
/>
) : (
<Select
value={additionalValues[field.name] || ""}
onValueChange={(value) => handleAdditionalFieldChange(field.name, value)}
>
<SelectTrigger className="w-full h-10">
<SelectValue placeholder={field.label} />
</SelectTrigger>
<SelectContent>
{field.options?.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,75 @@
import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching";
/**
* Hook for fetching data from Next.js API routes
* @param endpoint The API endpoint to fetch data from (e.g., '/api/applications')
* @param initialParams Initial request parameters
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
*/
export function useApiData<T>(
endpoint: string,
initialParams: Partial<RequestParams> = {}
) {
// Define the fetch function that will be passed to useDataFetching
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => {
try {
// Construct query parameters
const queryParams = new URLSearchParams();
// Add pagination parameters
queryParams.append("page", params.page.toString());
queryParams.append("size", params.size.toString());
// Add sorting parameters
if (params.orderField && params.orderField.length > 0) {
params.orderField.forEach((field, index) => {
queryParams.append("orderField", field);
if (params.orderType && params.orderType[index]) {
queryParams.append("orderType", params.orderType[index]);
}
});
}
// Add query filters
if (params.query && Object.keys(params.query).length > 0) {
Object.entries(params.query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
queryParams.append(key, value.toString());
}
});
}
// Make the API request
const response = await fetch(`${endpoint}?${queryParams.toString()}`);
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching data from API:", error);
// Return empty data with pagination info on error
return {
data: [],
pagination: {
page: params.page,
size: params.size,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: params.orderField,
orderType: params.orderType,
query: params.query,
next: false,
back: false,
},
};
}
};
// Use the generic data fetching hook with our API-specific fetch function
return useDataFetching<T>(fetchFromApi, initialParams);
}

View File

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

View File

@@ -0,0 +1,9 @@
// Export all components from the commons directory
export { CardDisplay } from './CardDisplay';
export { SearchComponent } from './SearchComponent';
export { ActionButtonsComponent } from './ActionButtonsComponent';
export { PaginationToolsComponent } from './PaginationToolsComponent';
// Export hooks
export { useDataFetching, type RequestParams, type ResponseMetadata, type PagePagination, type ApiResponse } from './hooks/useDataFetching';
export { useApiData } from './hooks/useApiData';

View File

@@ -0,0 +1,86 @@
Card Display which includes
/api/...
async POST somefunction() => /api/...
/page.tsx
I want create a nextjs api that fecth data instead having below code in schema
```tsx
export const fetchApplicationData = async ({
page = 1,
size = 10,
orderFields = ["name"],
orderTypes = ["asc"],
query = {},
}: {
page?: number;
size?: number;
orderFields?: string[];
orderTypes?: string[];
query?: Record<string, any>;
}) => {
// Call the actual API function
try {
const response = await listApplications({
page,
size,
orderField: orderFields,
orderType: orderTypes,
query,
});
return {
data: response.data,
pagination: response.pagination,
};
} catch (error) {
console.error("Error fetching application data:", error);
return {
data: [],
pagination: {
page,
size,
totalCount: 0,
totalItems: 0,
totalPages: 0,
pageCount: 0,
orderField: orderFields || [],
orderType: orderTypes || [],
query: {},
} as PagePagination,
};
}
};
```
I want all these components and default return of my external api which is
interface ApiResponse {
data: any[];
pagination: PagePagination;
}
@/components/schemas
@/components/commons/CardDisplay
@/components/commons/PaginationToolsComponent
@/components/commons/...ImportableComponents // other importable components
```tsx
const {data, pagination, loading, error, updatePagination, refetch} = fecthDataFromApi();
const showFields = ["uu_id", "Field1", "Field2"];
const [mode, setMode] = useState<"list" | "create" | "update">("list");
// Importable components
<ImportableComponents(Like Search, Select, Sort...)>
<CardDisplay
showFields={showFields}
data={data}
lang={lang}
translations={translations}
pagination={pagination}
updatePagination={updatePagination}
error={error}
loading={loading}
/>
```

View File

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

View File

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