Card Component implemented
This commit is contained in:
parent
113e43c7d7
commit
0052c92974
|
|
@ -525,22 +525,23 @@ def create_application_defaults(db_session):
|
||||||
sup_manager_employee.password_token = PasswordModule.generate_refresher_token()
|
sup_manager_employee.password_token = PasswordModule.generate_refresher_token()
|
||||||
with mongo_handler.collection(collection_name) as mongo_engine:
|
with mongo_handler.collection(collection_name) as mongo_engine:
|
||||||
existing_record = mongo_engine.find_one({"user_uu_id": str(sup_manager_employee.uu_id)})
|
existing_record = mongo_engine.find_one({"user_uu_id": str(sup_manager_employee.uu_id)})
|
||||||
|
|
||||||
if not existing_record:
|
if not existing_record:
|
||||||
|
print('insert sup existing record',existing_record)
|
||||||
mongo_engine.insert_one(
|
mongo_engine.insert_one(
|
||||||
document={
|
document={
|
||||||
"user_uu_id": str(sup_manager_employee.uu_id),
|
"user_uu_id": str(sup_manager_employee.uu_id),
|
||||||
"other_domains_list": [main_domain],
|
"other_domains_list": [main_domain, "management.com.tr"],
|
||||||
"main_domain": main_domain,
|
"main_domain": main_domain,
|
||||||
"modified_at": arrow.now().timestamp(),
|
"modified_at": arrow.now().timestamp(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
print('update sup existing record',existing_record)
|
||||||
# Optionally update the existing record if needed
|
# Optionally update the existing record if needed
|
||||||
mongo_engine.update_one(
|
mongo_engine.update_one(
|
||||||
{"user_uu_id": str(sup_manager_employee.uu_id)},
|
{"user_uu_id": str(sup_manager_employee.uu_id)},
|
||||||
{"$set": {
|
{"$set": {
|
||||||
"other_domains_list": [main_domain],
|
"other_domains_list": [main_domain, "management.com.tr"],
|
||||||
"main_domain": main_domain,
|
"main_domain": main_domain,
|
||||||
"modified_at": arrow.now().timestamp(),
|
"modified_at": arrow.now().timestamp(),
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,8 @@ def create_occupant_defaults(db_session):
|
||||||
user_tenant.password_token = PasswordModule.generate_refresher_token()
|
user_tenant.password_token = PasswordModule.generate_refresher_token()
|
||||||
|
|
||||||
with mongo_handler.collection(collection_name) as mongo_engine:
|
with mongo_handler.collection(collection_name) as mongo_engine:
|
||||||
|
existing_record = mongo_engine.find_one({"user_uu_id": str(user_build_manager.uu_id)})
|
||||||
|
if not existing_record:
|
||||||
mongo_engine.insert_one(
|
mongo_engine.insert_one(
|
||||||
document={
|
document={
|
||||||
"user_uu_id": str(user_build_manager.uu_id),
|
"user_uu_id": str(user_build_manager.uu_id),
|
||||||
|
|
@ -247,8 +249,19 @@ def create_occupant_defaults(db_session):
|
||||||
"modified_at": arrow.now().timestamp(),
|
"modified_at": arrow.now().timestamp(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
mongo_engine.update_one(
|
||||||
|
{"user_uu_id": str(user_build_manager.uu_id)},
|
||||||
|
{"$set": {
|
||||||
|
"other_domains_list": [main_domain],
|
||||||
|
"main_domain": main_domain,
|
||||||
|
"modified_at": arrow.now().timestamp(),
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
with mongo_handler.collection(collection_name) as mongo_engine:
|
with mongo_handler.collection(collection_name) as mongo_engine:
|
||||||
|
existing_record = mongo_engine.find_one({"user_uu_id": str(user_owner.uu_id)})
|
||||||
|
if not existing_record:
|
||||||
mongo_engine.insert_one(
|
mongo_engine.insert_one(
|
||||||
document={
|
document={
|
||||||
"user_uu_id": str(user_owner.uu_id),
|
"user_uu_id": str(user_owner.uu_id),
|
||||||
|
|
@ -257,8 +270,19 @@ def create_occupant_defaults(db_session):
|
||||||
"modified_at": arrow.now().timestamp(),
|
"modified_at": arrow.now().timestamp(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
mongo_engine.update_one(
|
||||||
|
{"user_uu_id": str(user_owner.uu_id)},
|
||||||
|
{"$set": {
|
||||||
|
"other_domains_list": [main_domain],
|
||||||
|
"main_domain": main_domain,
|
||||||
|
"modified_at": arrow.now().timestamp(),
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
with mongo_handler.collection(collection_name) as mongo_engine:
|
with mongo_handler.collection(collection_name) as mongo_engine:
|
||||||
|
existing_record = mongo_engine.find_one({"user_uu_id": str(user_tenant.uu_id)})
|
||||||
|
if not existing_record:
|
||||||
mongo_engine.insert_one(
|
mongo_engine.insert_one(
|
||||||
document={
|
document={
|
||||||
"user_uu_id": str(user_tenant.uu_id),
|
"user_uu_id": str(user_tenant.uu_id),
|
||||||
|
|
@ -267,6 +291,15 @@ def create_occupant_defaults(db_session):
|
||||||
"modified_at": arrow.now().timestamp(),
|
"modified_at": arrow.now().timestamp(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
mongo_engine.update_one(
|
||||||
|
{"user_uu_id": str(user_tenant.uu_id)},
|
||||||
|
{"$set": {
|
||||||
|
"other_domains_list": [main_domain],
|
||||||
|
"main_domain": main_domain,
|
||||||
|
"modified_at": arrow.now().timestamp(),
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
created_build_living_space_prs = BuildLivingSpace.find_or_create(
|
created_build_living_space_prs = BuildLivingSpace.find_or_create(
|
||||||
build_id=created_build.id,
|
build_id=created_build.id,
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,22 @@ npm config set legacy-peer-deps true
|
||||||
|
|
||||||
# Install base components
|
# Install base components
|
||||||
echo "🧩 Installing base shadcn/ui components..."
|
echo "🧩 Installing base shadcn/ui components..."
|
||||||
npx shadcn@latest add button --yes
|
npx shadcn@latest add button -y
|
||||||
npx shadcn@latest add form --yes
|
npx shadcn@latest add form -y
|
||||||
npx shadcn@latest add input --yes
|
npx shadcn@latest add input -y
|
||||||
npx shadcn@latest add label --yes
|
npx shadcn@latest add label -y
|
||||||
npx shadcn@latest add select --yes
|
npx shadcn@latest add select -y
|
||||||
npx shadcn@latest add checkbox --yes
|
npx shadcn@latest add checkbox -y
|
||||||
npx shadcn@latest add card --yes
|
npx shadcn@latest add card -y
|
||||||
npx shadcn@latest add dialog --yes
|
npx shadcn@latest add dialog -y
|
||||||
npx shadcn@latest add popover --yes
|
npx shadcn@latest add popover -y
|
||||||
npx shadcn@latest add sonner --yes
|
npx shadcn@latest add sonner -y
|
||||||
npx shadcn@latest add table --yes
|
npx shadcn@latest add table -y
|
||||||
npx shadcn@latest add pagination --yes
|
npx shadcn@latest add pagination -y
|
||||||
npx shadcn@latest add calendar --yes
|
npx shadcn@latest add calendar -y
|
||||||
npx shadcn@latest add date-picker --yes
|
npx shadcn@latest add date-picker -y
|
||||||
|
npx shadcn@latest add skeleton -y
|
||||||
|
npx shadcn@latest add table -y
|
||||||
|
|
||||||
# Update any dependencies with legacy peer deps
|
# Update any dependencies with legacy peer deps
|
||||||
echo "🔄 Updating dependencies..."
|
echo "🔄 Updating dependencies..."
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,22 @@ npm config set legacy-peer-deps true
|
||||||
|
|
||||||
# Install base components
|
# Install base components
|
||||||
echo "🧩 Installing base shadcn/ui components..."
|
echo "🧩 Installing base shadcn/ui components..."
|
||||||
npx shadcn@latest add button --yes
|
npx shadcn@latest add button -y
|
||||||
npx shadcn@latest add form --yes
|
npx shadcn@latest add form -y
|
||||||
npx shadcn@latest add input --yes
|
npx shadcn@latest add input -y
|
||||||
npx shadcn@latest add label --yes
|
npx shadcn@latest add label -y
|
||||||
npx shadcn@latest add select --yes
|
npx shadcn@latest add select -y
|
||||||
npx shadcn@latest add checkbox --yes
|
npx shadcn@latest add checkbox -y
|
||||||
npx shadcn@latest add card --yes
|
npx shadcn@latest add card -y
|
||||||
npx shadcn@latest add dialog --yes
|
npx shadcn@latest add dialog -y
|
||||||
npx shadcn@latest add popover --yes
|
npx shadcn@latest add popover -y
|
||||||
npx shadcn@latest add sonner --yes
|
npx shadcn@latest add sonner -y
|
||||||
npx shadcn@latest add table --yes
|
npx shadcn@latest add table -y
|
||||||
npx shadcn@latest add pagination --yes
|
npx shadcn@latest add pagination -y
|
||||||
npx shadcn@latest add calendar --yes
|
npx shadcn@latest add calendar -y
|
||||||
npx shadcn@latest add date-picker --yes
|
npx shadcn@latest add date-picker -y
|
||||||
|
npx shadcn@latest add skeleton -y
|
||||||
|
npx shadcn@latest add table -y
|
||||||
|
|
||||||
# Update any dependencies with legacy peer deps
|
# Update any dependencies with legacy peer deps
|
||||||
echo "🔄 Updating dependencies..."
|
echo "🔄 Updating dependencies..."
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { listApplications } from "@/apicalls/application/application";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Get query parameters
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
|
||||||
|
// Extract pagination parameters
|
||||||
|
const page = parseInt(searchParams.get("page") || "1");
|
||||||
|
const size = parseInt(searchParams.get("size") || "10");
|
||||||
|
|
||||||
|
// Extract sorting parameters
|
||||||
|
const orderField = searchParams.getAll("orderField") || ["name"];
|
||||||
|
const orderType = searchParams.getAll("orderType") || ["asc"];
|
||||||
|
|
||||||
|
// Extract query filters
|
||||||
|
const query: Record<string, any> = {};
|
||||||
|
for (const [key, value] of searchParams.entries()) {
|
||||||
|
if (!["page", "size", "orderField", "orderType"].includes(key)) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the actual API function
|
||||||
|
const response = await listApplications({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
orderField,
|
||||||
|
orderType,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the response
|
||||||
|
return NextResponse.json({
|
||||||
|
data: response.data || [],
|
||||||
|
pagination: response.pagination || {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
orderField,
|
||||||
|
orderType,
|
||||||
|
query,
|
||||||
|
next: false,
|
||||||
|
back: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Here you would call your actual API function to create a new application
|
||||||
|
// For example: const result = await createApplication(body);
|
||||||
|
|
||||||
|
// For now, we'll return a mock response
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: Math.floor(Math.random() * 1000),
|
||||||
|
...body,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
// Generic API handler that can be extended for different data types
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Get query parameters
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
|
||||||
|
// Extract pagination parameters
|
||||||
|
const page = parseInt(searchParams.get("page") || "1");
|
||||||
|
const size = parseInt(searchParams.get("size") || "10");
|
||||||
|
|
||||||
|
// Extract sorting parameters
|
||||||
|
const orderField = searchParams.getAll("orderField") || ["name"];
|
||||||
|
const orderType = searchParams.getAll("orderType") || ["asc"];
|
||||||
|
|
||||||
|
// Extract query filters
|
||||||
|
const query: Record<string, any> = {};
|
||||||
|
for (const [key, value] of searchParams.entries()) {
|
||||||
|
if (!["page", "size", "orderField", "orderType"].includes(key)) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is where you would call your actual data service
|
||||||
|
// For example: const result = await dataService.getData(page, size, orderField, orderType, query);
|
||||||
|
|
||||||
|
// Define the data type for our mock items
|
||||||
|
interface MockItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
[key: string]: string | number; // Index signature to allow string indexing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate mock data with application-specific fields
|
||||||
|
const allMockData: MockItem[] = Array.from({ length: 100 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: `Application ${i + 1}`,
|
||||||
|
description: `Description for application ${i + 1}`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
application_code: `APP-${1000 + i}`,
|
||||||
|
site_url: `/app-${i + 1}`,
|
||||||
|
application_type: i % 3 === 0 ? 'Web' : i % 3 === 1 ? 'Mobile' : 'Desktop',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Apply filtering based on query parameters
|
||||||
|
let filteredData = [...allMockData];
|
||||||
|
|
||||||
|
// Apply simple filtering for demonstration
|
||||||
|
if (Object.keys(query).length > 0) {
|
||||||
|
filteredData = filteredData.filter(item => {
|
||||||
|
return Object.entries(query).every(([key, value]) => {
|
||||||
|
// Handle special operators like __ilike
|
||||||
|
if (key.includes('__ilike')) {
|
||||||
|
const actualKey = key.split('__')[0];
|
||||||
|
const searchValue = String(value).replace(/%/g, '');
|
||||||
|
return String(item[actualKey]).toLowerCase().includes(searchValue.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle exact match
|
||||||
|
return String(item[key]).toLowerCase() === String(value).toLowerCase();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (orderField.length > 0 && orderType.length > 0) {
|
||||||
|
const field = orderField[0];
|
||||||
|
const direction = orderType[0];
|
||||||
|
|
||||||
|
filteredData.sort((a, b) => {
|
||||||
|
if (direction === 'asc') {
|
||||||
|
return String(a[field]).localeCompare(String(b[field]));
|
||||||
|
} else {
|
||||||
|
return String(b[field]).localeCompare(String(a[field]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate pagination metadata
|
||||||
|
const totalCount = filteredData.length;
|
||||||
|
const totalPages = Math.ceil(totalCount / size);
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
const startIndex = (page - 1) * size;
|
||||||
|
const endIndex = startIndex + size;
|
||||||
|
const paginatedData = filteredData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: paginatedData,
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
totalCount,
|
||||||
|
totalItems: totalCount,
|
||||||
|
totalPages,
|
||||||
|
pageCount: paginatedData.length,
|
||||||
|
orderField,
|
||||||
|
orderType,
|
||||||
|
query,
|
||||||
|
next: page < totalPages,
|
||||||
|
back: page > 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST handler for creating new items
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// This is where you would call your actual data service to create a new item
|
||||||
|
// For example: const result = await dataService.createItem(body);
|
||||||
|
|
||||||
|
// For now, we'll return a mock response
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: Math.floor(Math.random() * 1000),
|
||||||
|
...body,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,313 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { CardDisplay } from "@/components/commons/CardDisplay";
|
||||||
|
import { SearchComponent } from "@/components/commons/SearchComponent";
|
||||||
|
import { ActionButtonsComponent } from "@/components/commons/ActionButtonsComponent";
|
||||||
|
import { PaginationToolsComponent } from "@/components/commons/PaginationToolsComponent";
|
||||||
|
import { useApiData } from "@/components/commons/hooks/useApiData";
|
||||||
|
import { User, Building } from "lucide-react";
|
||||||
|
|
||||||
|
// Example translations
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
create: "Create",
|
||||||
|
update: "Update",
|
||||||
|
delete: "Delete",
|
||||||
|
view: "View",
|
||||||
|
search: "Search",
|
||||||
|
typeSelection: "Type Selection",
|
||||||
|
filterSelection: "Filter Selection",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
employee: "Employee",
|
||||||
|
occupant: "Occupant",
|
||||||
|
showing: "Showing",
|
||||||
|
of: "of",
|
||||||
|
items: "items",
|
||||||
|
total: "Total",
|
||||||
|
filtered: "Filtered",
|
||||||
|
previous: "Previous",
|
||||||
|
next: "Next",
|
||||||
|
page: "Page",
|
||||||
|
itemsPerPage: "Items per page",
|
||||||
|
noData: "No data found",
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Name",
|
||||||
|
description: "Description",
|
||||||
|
createdAt: "Created At",
|
||||||
|
type: "Type",
|
||||||
|
status: "Status",
|
||||||
|
application_code: "Code",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Type"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
create: "Oluştur",
|
||||||
|
update: "Güncelle",
|
||||||
|
delete: "Sil",
|
||||||
|
view: "Görüntüle",
|
||||||
|
search: "Ara",
|
||||||
|
typeSelection: "Tür Seçimi",
|
||||||
|
filterSelection: "Filtre Seçimi",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
employee: "Çalışan",
|
||||||
|
occupant: "Sakin",
|
||||||
|
showing: "Gösteriliyor",
|
||||||
|
of: "of",
|
||||||
|
items: "öğeler",
|
||||||
|
total: "Toplam",
|
||||||
|
filtered: "Filtreli",
|
||||||
|
previous: "Önceki",
|
||||||
|
next: "Sonraki",
|
||||||
|
page: "Sayfa",
|
||||||
|
itemsPerPage: "Sayfa başına öğeler",
|
||||||
|
noData: "Veri bulunamadı",
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Ad",
|
||||||
|
description: "Açıklama",
|
||||||
|
createdAt: "Oluşturulma Tarihi",
|
||||||
|
type: "Tür",
|
||||||
|
status: "Durum",
|
||||||
|
application_code: "Kod",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Tür"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the data type
|
||||||
|
interface ApplicationData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form component for create/update
|
||||||
|
const FormComponent: React.FC<{
|
||||||
|
initialData?: ApplicationData;
|
||||||
|
mode: "create" | "update";
|
||||||
|
refetch?: () => void;
|
||||||
|
setMode: React.Dispatch<React.SetStateAction<"list" | "create" | "update">>;
|
||||||
|
setSelectedItem: React.Dispatch<React.SetStateAction<ApplicationData | null>>;
|
||||||
|
onCancel: () => void;
|
||||||
|
lang: string;
|
||||||
|
}> = ({ initialData, mode, refetch, setMode, onCancel, lang }) => {
|
||||||
|
// In a real application, you would implement form fields and submission logic here
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-lg">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{mode === "create" ? translations[lang as "en" | "tr"].create : translations[lang as "en" | "tr"].update}
|
||||||
|
</h2>
|
||||||
|
<p>This is a placeholder for the {mode} form.</p>
|
||||||
|
<div className="mt-4 flex space-x-2">
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-gray-200 rounded"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
|
onClick={() => {
|
||||||
|
// In a real app, you would submit the form data here
|
||||||
|
if (refetch) refetch();
|
||||||
|
setMode("list");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CardExamplePage() {
|
||||||
|
const [lang, setLang] = useState<"en" | "tr">("en");
|
||||||
|
const [mode, setMode] = useState<"list" | "create" | "update">("list");
|
||||||
|
const [selectedItem, setSelectedItem] = useState<ApplicationData | null>(null);
|
||||||
|
const [gridCols, setGridCols] = useState<1 | 2 | 3 | 4 | 5 | 6>(3);
|
||||||
|
|
||||||
|
// Use the API data hook
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch
|
||||||
|
} = useApiData<ApplicationData>("/api/data");
|
||||||
|
|
||||||
|
// Fields to display in the cards
|
||||||
|
const showFields = ["application_code", "site_url", "application_type"];
|
||||||
|
|
||||||
|
// Search options
|
||||||
|
const searchOptions = {
|
||||||
|
typeOptions: [
|
||||||
|
{
|
||||||
|
value: "employee",
|
||||||
|
label: translations[lang].employee,
|
||||||
|
icon: <User className="mr-2 h-4 w-4" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "occupant",
|
||||||
|
label: translations[lang].occupant,
|
||||||
|
icon: <Building className="mr-2 h-4 w-4" />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
urlOptions: [
|
||||||
|
"/dashboard",
|
||||||
|
"/individual",
|
||||||
|
"/user",
|
||||||
|
"/settings",
|
||||||
|
"/reports",
|
||||||
|
],
|
||||||
|
additionalFields: [
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
label: translations[lang as "en" | "tr"].status,
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ value: "active", label: "Active" },
|
||||||
|
{ value: "inactive", label: "Inactive" },
|
||||||
|
{ value: "pending", label: "Pending" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle search
|
||||||
|
const handleSearch = (query: Record<string, string>) => {
|
||||||
|
updatePagination({
|
||||||
|
page: 1, // Reset to first page on new search
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle card actions
|
||||||
|
const handleCardClick = (item: ApplicationData) => {
|
||||||
|
console.log("Card clicked:", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewClick = (item: ApplicationData) => {
|
||||||
|
console.log("View clicked:", item);
|
||||||
|
// Example: Open a modal to view details
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateClick = (item: ApplicationData) => {
|
||||||
|
console.log("Update clicked:", item);
|
||||||
|
setSelectedItem(item);
|
||||||
|
setMode("update");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle create button click
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
setSelectedItem(null);
|
||||||
|
setMode("create");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
const handleCancel = () => {
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<div className="mb-4 flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold">Card Example Page</h1>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2 text-sm">Grid Size:</span>
|
||||||
|
<select
|
||||||
|
value={gridCols}
|
||||||
|
onChange={(e) => setGridCols(Number(e.target.value) as 1 | 2 | 3 | 4 | 5 | 6)}
|
||||||
|
className="p-2 border rounded"
|
||||||
|
>
|
||||||
|
<option value="1">1 Column</option>
|
||||||
|
<option value="2">2 Columns</option>
|
||||||
|
<option value="3">3 Columns</option>
|
||||||
|
<option value="4">4 Columns</option>
|
||||||
|
<option value="5">5 Columns</option>
|
||||||
|
<option value="6">6 Columns</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={lang}
|
||||||
|
onChange={(e) => setLang(e.target.value as "en" | "tr")}
|
||||||
|
className="p-2 border rounded"
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="tr">Turkish</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === "list" ? (
|
||||||
|
<>
|
||||||
|
{/* Search Component */}
|
||||||
|
<SearchComponent
|
||||||
|
onSearch={handleSearch}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
typeOptions={searchOptions.typeOptions}
|
||||||
|
urlOptions={searchOptions.urlOptions}
|
||||||
|
additionalFields={searchOptions.additionalFields}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Action Buttons Component */}
|
||||||
|
<ActionButtonsComponent
|
||||||
|
onCreateClick={handleCreateClick}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Card Display Component */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Tools Component */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<PaginationToolsComponent
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
loading={loading}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FormComponent
|
||||||
|
initialData={selectedItem || undefined}
|
||||||
|
mode={mode}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { CardDisplay, useApiData } from "@/components/commons";
|
||||||
|
import { User, Building } from "lucide-react";
|
||||||
|
|
||||||
|
// Example translations
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
create: "Create",
|
||||||
|
update: "Update",
|
||||||
|
delete: "Delete",
|
||||||
|
view: "View",
|
||||||
|
search: "Search",
|
||||||
|
typeSelection: "Type Selection",
|
||||||
|
filterSelection: "Filter Selection",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
employee: "Employee",
|
||||||
|
occupant: "Occupant",
|
||||||
|
showing: "Showing",
|
||||||
|
of: "of",
|
||||||
|
items: "items",
|
||||||
|
total: "Total",
|
||||||
|
filtered: "Filtered",
|
||||||
|
previous: "Previous",
|
||||||
|
next: "Next",
|
||||||
|
page: "Page",
|
||||||
|
itemsPerPage: "Items per page",
|
||||||
|
noData: "No data found",
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Name",
|
||||||
|
description: "Description",
|
||||||
|
createdAt: "Created At",
|
||||||
|
type: "Type",
|
||||||
|
status: "Status"
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
create: "Oluştur",
|
||||||
|
update: "Güncelle",
|
||||||
|
delete: "Sil",
|
||||||
|
view: "Görüntüle",
|
||||||
|
search: "Ara",
|
||||||
|
typeSelection: "Tür Seçimi",
|
||||||
|
filterSelection: "Filtre Seçimi",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
employee: "Çalışan",
|
||||||
|
occupant: "Sakin",
|
||||||
|
showing: "Gösteriliyor",
|
||||||
|
of: "of",
|
||||||
|
items: "öğeler",
|
||||||
|
total: "Toplam",
|
||||||
|
filtered: "Filtreli",
|
||||||
|
previous: "Önceki",
|
||||||
|
next: "Sonraki",
|
||||||
|
page: "Sayfa",
|
||||||
|
itemsPerPage: "Sayfa başına öğeler",
|
||||||
|
noData: "Veri bulunamadı",
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Ad",
|
||||||
|
description: "Açıklama",
|
||||||
|
createdAt: "Oluşturulma Tarihi",
|
||||||
|
type: "Tür",
|
||||||
|
status: "Durum"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the data type
|
||||||
|
interface ExampleData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
type?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form component for create/update
|
||||||
|
const FormComponent: React.FC<{
|
||||||
|
initialData?: ExampleData;
|
||||||
|
mode: "create" | "update";
|
||||||
|
refetch?: () => void;
|
||||||
|
setMode: React.Dispatch<React.SetStateAction<"list" | "create" | "update">>;
|
||||||
|
setSelectedItem: React.Dispatch<React.SetStateAction<ExampleData | null>>;
|
||||||
|
onCancel: () => void;
|
||||||
|
lang: string;
|
||||||
|
}> = ({ initialData, mode, refetch, setMode, onCancel, lang }) => {
|
||||||
|
// In a real application, you would implement form fields and submission logic here
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-lg">
|
||||||
|
<h2 className="text-xl font-bold mb-4">
|
||||||
|
{mode === "create" ? translations[lang as "en" | "tr"].create : translations[lang as "en" | "tr"].update}
|
||||||
|
</h2>
|
||||||
|
<p>This is a placeholder for the {mode} form.</p>
|
||||||
|
<div className="mt-4 flex space-x-2">
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-gray-200 rounded"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
|
onClick={() => {
|
||||||
|
// In a real app, you would submit the form data here
|
||||||
|
if (refetch) refetch();
|
||||||
|
setMode("list");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ExamplePage() {
|
||||||
|
const [lang, setLang] = useState<"en" | "tr">("en");
|
||||||
|
|
||||||
|
// Use the API data hook
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch
|
||||||
|
} = useApiData<ExampleData>("/api/data");
|
||||||
|
|
||||||
|
// Fields to display in the table
|
||||||
|
const showFields = ["id", "name", "description", "createdAt"];
|
||||||
|
|
||||||
|
// Search options
|
||||||
|
const searchOptions = {
|
||||||
|
typeOptions: [
|
||||||
|
{
|
||||||
|
value: "employee",
|
||||||
|
label: translations[lang as "en" | "tr"].employee,
|
||||||
|
icon: <User className="mr-2 h-4 w-4" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "occupant",
|
||||||
|
label: translations[lang as "en" | "tr"].occupant,
|
||||||
|
icon: <Building className="mr-2 h-4 w-4" />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
urlOptions: [
|
||||||
|
"/dashboard",
|
||||||
|
"/individual",
|
||||||
|
"/user",
|
||||||
|
"/settings",
|
||||||
|
"/reports",
|
||||||
|
],
|
||||||
|
additionalFields: [
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
label: translations[lang as "en" | "tr"].status,
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ value: "active", label: "Active" },
|
||||||
|
{ value: "inactive", label: "Inactive" },
|
||||||
|
{ value: "pending", label: "Pending" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom cell renderer example
|
||||||
|
const renderCustomCell = (item: ExampleData, field: string) => {
|
||||||
|
if (field === "createdAt") {
|
||||||
|
return new Date(item.createdAt).toLocaleDateString();
|
||||||
|
}
|
||||||
|
return item[field as keyof ExampleData];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<div className="mb-4 flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold">Example Page</h1>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={lang}
|
||||||
|
onChange={(e) => setLang(e.target.value as "en" | "tr")}
|
||||||
|
className="p-2 border rounded"
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="tr">Turkish</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardDisplay<ExampleData>
|
||||||
|
showFields={showFields}
|
||||||
|
data={data}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
error={error}
|
||||||
|
loading={loading}
|
||||||
|
refetch={refetch}
|
||||||
|
searchOptions={searchOptions}
|
||||||
|
renderCustomCell={renderCustomCell}
|
||||||
|
FormComponent={FormComponent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,9 @@ export const SearchComponent: React.FC<SearchComponentProps> = ({
|
||||||
if (url) {
|
if (url) {
|
||||||
searchParams.site_url = url;
|
searchParams.site_url = url;
|
||||||
}
|
}
|
||||||
|
if (query) {
|
||||||
searchParams.name = query
|
searchParams.name = query
|
||||||
|
}
|
||||||
searchParams.application_for = type === "employee" ? "EMP" : "OCC";
|
searchParams.application_for = type === "employee" ? "EMP" : "OCC";
|
||||||
|
|
||||||
// Call onSearch with the search parameters
|
// Call onSearch with the search parameters
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { CardItem } from "./CardItem";
|
||||||
|
import { CardSkeleton } from "./CardSkeleton";
|
||||||
|
import { getFieldValue, getGridClasses } from "./utils";
|
||||||
|
import { CardDisplayProps } from "./schema";
|
||||||
|
|
||||||
|
// Interface moved to schema.ts
|
||||||
|
|
||||||
|
export function CardDisplay<T>({
|
||||||
|
showFields,
|
||||||
|
data,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
titleField = "name",
|
||||||
|
onCardClick,
|
||||||
|
renderCustomField,
|
||||||
|
gridCols = 4,
|
||||||
|
showViewIcon = false,
|
||||||
|
showUpdateIcon = false,
|
||||||
|
onViewClick,
|
||||||
|
onUpdateClick,
|
||||||
|
}: CardDisplayProps<T>) {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center text-red-500">
|
||||||
|
{error.message || "An error occurred while fetching data."}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={getGridClasses(gridCols)}>
|
||||||
|
{loading ? (
|
||||||
|
// Loading skeletons
|
||||||
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
|
<CardSkeleton
|
||||||
|
key={`loading-${index}`}
|
||||||
|
index={index}
|
||||||
|
showFields={showFields}
|
||||||
|
showViewIcon={showViewIcon}
|
||||||
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="col-span-full text-center py-6">
|
||||||
|
{(translations[lang] || {}).noData || "No data found"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item, index) => (
|
||||||
|
<CardItem
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
showFields={showFields}
|
||||||
|
titleField={titleField}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
onCardClick={onCardClick}
|
||||||
|
renderCustomField={renderCustomField}
|
||||||
|
showViewIcon={showViewIcon}
|
||||||
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
onViewClick={onViewClick}
|
||||||
|
onUpdateClick={onUpdateClick}
|
||||||
|
getFieldValue={getFieldValue}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { CardSkeletonProps } from "./schema";
|
||||||
|
|
||||||
|
// Interface moved to schema.ts
|
||||||
|
|
||||||
|
export function CardSkeleton({
|
||||||
|
index,
|
||||||
|
showFields,
|
||||||
|
showViewIcon,
|
||||||
|
showUpdateIcon,
|
||||||
|
}: CardSkeletonProps) {
|
||||||
|
return (
|
||||||
|
<div key={`loading-${index}`} className="w-full p-1">
|
||||||
|
<Card className="h-full">
|
||||||
|
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
||||||
|
<Skeleton className="h-5 w-3/4" />
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{showViewIcon && (
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
)}
|
||||||
|
{showUpdateIcon && (
|
||||||
|
<Skeleton className="h-8 w-8 rounded-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{showFields.map((field, fieldIndex) => (
|
||||||
|
<div key={`loading-${index}-${field}`} className="flex">
|
||||||
|
<Skeleton className="h-4 w-10 mr-2" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { CardDisplay } from './CardDisplay';
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* CardDisplay component interfaces
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main props for the CardDisplay component
|
||||||
|
*/
|
||||||
|
export interface CardDisplayProps<T> {
|
||||||
|
/** Fields to display in each card */
|
||||||
|
showFields: string[];
|
||||||
|
/** Array of data items to display */
|
||||||
|
data: T[];
|
||||||
|
/** Current language code */
|
||||||
|
lang: string;
|
||||||
|
/** Translations object for field labels and messages */
|
||||||
|
translations: Record<string, any>;
|
||||||
|
/** Error object if data fetching failed */
|
||||||
|
error: Error | null;
|
||||||
|
/** Loading state indicator */
|
||||||
|
loading: boolean;
|
||||||
|
/** Field to use as the card title (default: "name") */
|
||||||
|
titleField?: string;
|
||||||
|
/** Handler for when a card is clicked */
|
||||||
|
onCardClick?: (item: T) => void;
|
||||||
|
/** Custom renderer for specific fields */
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
/** Number of columns in the grid (1-6) */
|
||||||
|
gridCols?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
/** Whether to show the view icon */
|
||||||
|
showViewIcon?: boolean;
|
||||||
|
/** Whether to show the update/edit icon */
|
||||||
|
showUpdateIcon?: boolean;
|
||||||
|
/** Handler for when the view icon is clicked */
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
/** Handler for when the update/edit icon is clicked */
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CardItem component
|
||||||
|
*/
|
||||||
|
export interface CardItemProps<T> {
|
||||||
|
/** Data item to display */
|
||||||
|
item: T;
|
||||||
|
/** Index of the item in the data array */
|
||||||
|
index: number;
|
||||||
|
/** Fields to display in the card */
|
||||||
|
showFields: string[];
|
||||||
|
/** Field to use as the card title */
|
||||||
|
titleField: string;
|
||||||
|
/** Current language code */
|
||||||
|
lang: string;
|
||||||
|
/** Translations object for field labels */
|
||||||
|
translations: Record<string, any>;
|
||||||
|
/** Handler for when the card is clicked */
|
||||||
|
onCardClick?: (item: T) => void;
|
||||||
|
/** Custom renderer for specific fields */
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
/** Whether to show the view icon */
|
||||||
|
showViewIcon: boolean;
|
||||||
|
/** Whether to show the update/edit icon */
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
/** Handler for when the view icon is clicked */
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
/** Handler for when the update/edit icon is clicked */
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
/** Function to get field values from the item */
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CardActions component
|
||||||
|
*/
|
||||||
|
export interface CardActionsProps<T> {
|
||||||
|
/** Data item the actions apply to */
|
||||||
|
item: T;
|
||||||
|
/** Whether to show the view icon */
|
||||||
|
showViewIcon: boolean;
|
||||||
|
/** Whether to show the update/edit icon */
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
/** Handler for when the view icon is clicked */
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
/** Handler for when the update/edit icon is clicked */
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CardField component
|
||||||
|
*/
|
||||||
|
export interface CardFieldProps<T> {
|
||||||
|
/** Data item the field belongs to */
|
||||||
|
item: T;
|
||||||
|
/** Field name to display */
|
||||||
|
field: string;
|
||||||
|
/** Current language code */
|
||||||
|
lang: string;
|
||||||
|
/** Translations object for field labels */
|
||||||
|
translations: Record<string, any>;
|
||||||
|
/** Custom renderer for specific fields */
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
/** Function to get field values from the item */
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CardSkeleton component
|
||||||
|
*/
|
||||||
|
export interface CardSkeletonProps {
|
||||||
|
/** Index of the skeleton in the loading array */
|
||||||
|
index: number;
|
||||||
|
/** Fields to create skeleton placeholders for */
|
||||||
|
showFields: string[];
|
||||||
|
/** Whether to show a skeleton for the view icon */
|
||||||
|
showViewIcon: boolean;
|
||||||
|
/** Whether to show a skeleton for the update/edit icon */
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Safely gets a field value from an item, supporting nested fields with dot notation
|
||||||
|
*/
|
||||||
|
export function getFieldValue(item: any, field: string): any {
|
||||||
|
if (!item) return "";
|
||||||
|
|
||||||
|
// Handle nested fields with dot notation (e.g., "user.name")
|
||||||
|
if (field.includes(".")) {
|
||||||
|
const parts = field.split(".");
|
||||||
|
let value = item;
|
||||||
|
for (const part of parts) {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
value = value[part];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a field label from translations or formats the field name
|
||||||
|
*/
|
||||||
|
export function getFieldLabel(field: string, translations: Record<string, any>, lang: string): string {
|
||||||
|
const t = translations[lang] || {};
|
||||||
|
return t[field] || field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates responsive grid classes based on the gridCols prop
|
||||||
|
*/
|
||||||
|
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
|
||||||
|
const baseClass = "grid grid-cols-1 gap-4";
|
||||||
|
|
||||||
|
// Map gridCols to responsive classes
|
||||||
|
const colClasses: Record<number, string> = {
|
||||||
|
1: "",
|
||||||
|
2: "sm:grid-cols-2",
|
||||||
|
3: "sm:grid-cols-2 md:grid-cols-3",
|
||||||
|
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
||||||
|
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
|
||||||
|
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"
|
||||||
|
};
|
||||||
|
|
||||||
|
return `${baseClass} ${colClasses[gridCols]}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
|
|
@ -0,0 +1,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,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue