components stablized

This commit is contained in:
2025-04-30 00:44:13 +03:00
parent 0052c92974
commit f2cc7a69b5
81 changed files with 3159 additions and 2299 deletions

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

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]}`;
}