updated left menu and page template

This commit is contained in:
berkay 2025-04-16 21:04:53 +03:00
parent dd4a8f333d
commit da95b629ac
8 changed files with 224 additions and 182 deletions

View File

@ -1,11 +1,10 @@
import React from 'react';
import { DataType, Pagination } from './schema';
import { getTranslation, LanguageKey } from './language';
import { ActionButtonsComponent } from './ActionButtonsComponent';
import { SortingComponent } from './SortingComponent';
import { PaginationToolsComponent } from './PaginationToolsComponent';
import React from "react";
import { DataType } from "./schema";
import { getTranslation, LanguageKey } from "./language";
import { ActionButtonsComponent } from "./ActionButtonsComponent";
import { SortingComponent } from "./SortingComponent";
import { PaginationToolsComponent } from "./PaginationToolsComponent";
import { PagePagination } from "@/components/validations/list/paginations";
interface DataCardProps {
item: DataType;
@ -14,11 +13,11 @@ interface DataCardProps {
lang?: LanguageKey;
}
export function DataCard({
item,
onView,
export function DataCard({
item,
onView,
onUpdate,
lang = 'en'
lang = "en",
}: DataCardProps) {
const t = getTranslation(lang);
@ -29,11 +28,18 @@ export function DataCard({
<h3 className="text-lg font-semibold">{item.title}</h3>
<p className="text-gray-600 mt-1">{item.description}</p>
<div className="mt-2 flex items-center">
<span className={`px-2 py-1 rounded text-xs ${item.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
<span
className={`px-2 py-1 rounded text-xs ${
item.status === "active"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{item.status}
</span>
<span className="text-xs text-gray-500 ml-2">
{t.formLabels.createdAt}: {new Date(item.createdAt).toLocaleDateString()}
{t.formLabels.createdAt}:{" "}
{new Date(item.createdAt).toLocaleDateString()}
</span>
</div>
</div>
@ -58,10 +64,10 @@ export function DataCard({
interface ListInfoComponentProps {
data: DataType[];
pagination: Pagination;
pagination: PagePagination;
loading: boolean;
error: Error | null;
updatePagination: (updates: Partial<Pagination>) => void;
updatePagination: (updates: Partial<PagePagination>) => void;
onCreateClick: () => void;
onViewClick: (item: DataType) => void;
onUpdateClick: (item: DataType) => void;
@ -77,7 +83,7 @@ export function ListInfoComponent({
onCreateClick,
onViewClick,
onUpdateClick,
lang = 'en'
lang = "en",
}: ListInfoComponentProps) {
const t = getTranslation(lang);
@ -91,30 +97,27 @@ export function ListInfoComponent({
return (
<>
<ActionButtonsComponent
onCreateClick={onCreateClick}
lang={lang}
/>
<SortingComponent
<ActionButtonsComponent onCreateClick={onCreateClick} lang={lang} />
<SortingComponent
pagination={pagination}
updatePagination={updatePagination}
lang={lang}
/>
<PaginationToolsComponent
<PaginationToolsComponent
pagination={pagination}
updatePagination={updatePagination}
lang={lang}
/>
{loading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : (
<div className="grid gap-4">
{data.map(item => (
{data.map((item) => (
<DataCard
key={item.id}
item={item}
@ -123,7 +126,7 @@ export function ListInfoComponent({
lang={lang}
/>
))}
{data.length === 0 && (
<div className="text-center py-12 text-gray-500">
{t.noItemsFound}

View File

@ -1,17 +1,17 @@
import React from 'react';
import { Pagination } from './schema';
import { getTranslation, LanguageKey } from './language';
import React from "react";
import { getTranslation, LanguageKey } from "./language";
import { PagePagination } from "@/components/validations/list/paginations";
interface PaginationToolsComponentProps {
pagination: Pagination;
updatePagination: (updates: Partial<Pagination>) => void;
pagination: PagePagination;
updatePagination: (updates: Partial<PagePagination>) => void;
lang?: LanguageKey;
}
export function PaginationToolsComponent({
pagination,
updatePagination,
lang = 'en'
lang = "en",
}: PaginationToolsComponentProps) {
const t = getTranslation(lang);
@ -30,19 +30,19 @@ export function PaginationToolsComponent({
<div className="flex flex-wrap justify-between items-center gap-4">
{/* Navigation buttons */}
<div className="flex items-center space-x-2">
<button
<button
onClick={() => handlePageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
className="px-3 py-1 bg-gray-200 rounded disabled:opacity-50"
>
{t.previous}
</button>
<span className="px-4 py-1">
{t.page} {pagination.page} {t.of} {pagination.totalPages}
</span>
<button
<button
onClick={() => handlePageChange(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
className="px-3 py-1 bg-gray-200 rounded disabled:opacity-50"
@ -50,26 +50,35 @@ export function PaginationToolsComponent({
{t.next}
</button>
</div>
{/* Items per page selector */}
<div className="flex items-center space-x-2">
<label htmlFor="page-size" className="text-sm font-medium">{t.itemsPerPage}</label>
<label htmlFor="page-size" className="text-sm font-medium">
{t.itemsPerPage}
</label>
<select
id="page-size"
value={pagination.size}
onChange={handleSizeChange}
className="border rounded px-2 py-1"
>
{[5, 10, 20, 50].map(size => (
<option key={size} value={size}>{size}</option>
{[5, 10, 20, 50].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
</div>
{/* Pagination stats */}
<div className="text-sm text-gray-600">
<div>{t.showing} {pagination.pageCount} {t.of} {pagination.totalCount} {t.items}</div>
<div>{t.total}: {pagination.allCount} {t.items}</div>
<div>
{t.showing} {pagination.pageCount} {t.of} {pagination.totalCount}{" "}
{t.items}
</div>
<div>
{t.total}: {pagination.allCount} {t.items}
</div>
</div>
</div>
</div>

View File

@ -1,15 +1,18 @@
import React, { useState, useEffect } from 'react';
import { DataSchema } from './schema';
import { getTranslation, LanguageKey } from './language';
import React, { useState, useEffect } from "react";
import { DataSchema } from "./schema";
import { getTranslation, LanguageKey } from "./language";
interface SearchComponentProps {
onSearch: (query: Record<string, string>) => void;
lang?: LanguageKey;
}
export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps) {
export function SearchComponent({
onSearch,
lang = "en",
}: SearchComponentProps) {
const t = getTranslation(lang);
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState("");
const [activeFields, setActiveFields] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<Record<string, string>>({});
@ -19,22 +22,22 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
// or if we have no active fields (to clear the search)
if ((activeFields.length > 0 && searchValue) || activeFields.length === 0) {
const newQuery: Record<string, string> = {};
// Only add fields if we have a search value
if (searchValue) {
activeFields.forEach(field => {
activeFields.forEach((field) => {
newQuery[field] = searchValue;
});
}
// Update local state
setSearchQuery(newQuery);
// Don't call onSearch here - it creates an infinite loop
// We'll call it in a separate effect
}
}, [activeFields, searchValue]);
// This effect handles calling the onSearch callback
// It runs when searchQuery changes, not when onSearch changes
useEffect(() => {
@ -44,7 +47,7 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchValue(value);
if (!value) {
setSearchQuery({});
onSearch({});
@ -57,18 +60,18 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
}
const newQuery: Record<string, string> = {};
activeFields.forEach(field => {
activeFields.forEach((field) => {
newQuery[field] = value;
});
setSearchQuery(newQuery);
onSearch(newQuery);
};
const toggleField = (field: string) => {
setActiveFields(prev => {
setActiveFields((prev) => {
if (prev.includes(field)) {
return prev.filter(f => f !== field);
return prev.filter((f) => f !== field);
} else {
return [...prev, field];
}
@ -79,29 +82,31 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
<div className="bg-white p-4 rounded-lg shadow mb-4">
<div className="mb-3">
<label htmlFor="search" className="block text-sm font-medium mb-1">
{t.search || 'Search'}
{t.search || "Search"}
</label>
<input
type="text"
id="search"
value={searchValue}
onChange={handleSearchChange}
placeholder={t.searchPlaceholder || 'Enter search term...'}
placeholder={t.searchPlaceholder || "Enter search term..."}
className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<div className="text-sm font-medium mb-1">{t.searchFields || 'Search in fields'}:</div>
<div className="text-sm font-medium mb-1">
{t.searchFields || "Search in fields"}:
</div>
<div className="flex flex-wrap gap-2">
{Object.keys(DataSchema.shape).map(field => (
{Object.keys(DataSchema.shape).map((field) => (
<button
key={field}
onClick={() => toggleField(field)}
className={`px-3 py-1 text-sm rounded ${
activeFields.includes(field)
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700'
activeFields.includes(field)
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700"
}`}
>
{field}
@ -109,15 +114,20 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
))}
</div>
</div>
{Object.keys(searchQuery).length > 0 && (
<div className="mt-3 p-2 bg-gray-100 rounded">
<div className="text-sm font-medium mb-1">{t.activeSearch || 'Active search'}:</div>
<div className="text-sm font-medium mb-1">
{t.activeSearch || "Active search"}:
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(searchQuery).map(([field, value]) => (
<div key={field} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
<div
key={field}
className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm"
>
{field}: {value}
<button
<button
onClick={() => toggleField(field)}
className="ml-2 text-blue-500 hover:text-blue-700"
>

View File

@ -1,41 +1,42 @@
import React from 'react';
import { Pagination, DataSchema } from './schema';
import { getTranslation, LanguageKey } from './language';
import React from "react";
import { DataSchema } from "./schema";
import { getTranslation, LanguageKey } from "./language";
import { PagePagination } from "@/components/validations/list/paginations";
interface SortingComponentProps {
pagination: Pagination;
updatePagination: (updates: Partial<Pagination>) => void;
pagination: PagePagination;
updatePagination: (updates: Partial<PagePagination>) => void;
lang?: LanguageKey;
}
export function SortingComponent({
pagination,
updatePagination,
lang = 'en'
lang = "en",
}: SortingComponentProps) {
const t = getTranslation(lang);
const handleSortChange = (field: string) => {
// Find if the field is already in the orderFields array
const fieldIndex = pagination.orderFields.indexOf(field);
// Create copies of the arrays to modify
const newOrderFields = [...pagination.orderFields];
const newOrderTypes = [...pagination.orderTypes];
if (fieldIndex === -1) {
// Field is not being sorted yet - add it with 'asc' direction
newOrderFields.push(field);
newOrderTypes.push('asc');
} else if (pagination.orderTypes[fieldIndex] === 'asc') {
newOrderTypes.push("asc");
} else if (pagination.orderTypes[fieldIndex] === "asc") {
// Field is being sorted ascending - change to descending
newOrderTypes[fieldIndex] = 'desc';
newOrderTypes[fieldIndex] = "desc";
} else {
// Field is being sorted descending - remove it from sorting
newOrderFields.splice(fieldIndex, 1);
newOrderTypes.splice(fieldIndex, 1);
}
updatePagination({
orderFields: newOrderFields,
orderTypes: newOrderTypes,
@ -48,22 +49,26 @@ export function SortingComponent({
<div className="flex items-center space-x-2">
<label className="text-sm font-medium">{t.sortBy}</label>
<div className="flex flex-wrap gap-2">
{Object.keys(DataSchema.shape).map(field => {
{Object.keys(DataSchema.shape).map((field) => {
// Find if this field is in the orderFields array
const fieldIndex = pagination.orderFields.indexOf(field);
const isActive = fieldIndex !== -1;
const direction = isActive ? pagination.orderTypes[fieldIndex] : null;
const direction = isActive
? pagination.orderTypes[fieldIndex]
: null;
return (
<button
key={field}
onClick={() => handleSortChange(field)}
className={`px-3 py-1 rounded ${isActive ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
className={`px-3 py-1 rounded ${
isActive ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
>
{field}
{isActive && (
<span className="ml-1">
{direction === 'asc' ? '↑' : '↓'}
{direction === "asc" ? "↑" : "↓"}
</span>
)}
</button>

View File

@ -1,36 +1,24 @@
import { useState, useEffect, useCallback } from 'react';
import { DataType, Pagination, fetchData, DataSchema } from './schema';
// Define request parameters interface
interface RequestParams {
page: number;
size: number;
orderFields: string[];
orderTypes: string[];
query: Record<string, string>;
}
// Define response metadata interface
interface ResponseMetadata {
totalCount: number;
allCount: number;
totalPages: number;
pageCount: number;
}
import { useState, useEffect, useCallback } from "react";
import { DataType, fetchData, DataSchema } from "./schema";
import {
PagePagination,
RequestParams,
ResponseMetadata,
} from "@/components/validations/list/paginations";
// Custom hook for pagination and data fetching
export function usePaginatedData() {
const [data, setData] = useState<DataType[]>([]);
// Request parameters - these are controlled by the user
const [requestParams, setRequestParams] = useState<RequestParams>({
page: 1,
size: 10,
orderFields: ['createdAt'],
orderTypes: ['desc'],
orderFields: ["createdAt"],
orderTypes: ["desc"],
query: {},
});
// Response metadata - these come from the API
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
totalCount: 0,
@ -38,7 +26,7 @@ export function usePaginatedData() {
totalPages: 0,
pageCount: 0,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
@ -52,19 +40,21 @@ export function usePaginatedData() {
orderTypes: requestParams.orderTypes,
query: requestParams.query,
});
// Validate data with Zod
const validatedData = result.data.map(item => {
try {
return DataSchema.parse(item);
} catch (err) {
console.error('Validation error for item:', item, err);
return null;
}
}).filter(Boolean) as DataType[];
const validatedData = result.data
.map((item) => {
try {
return DataSchema.parse(item);
} catch (err) {
console.error("Validation error for item:", item, err);
return null;
}
})
.filter(Boolean) as DataType[];
setData(validatedData);
// Update response metadata from API response
setResponseMetadata({
totalCount: result.pagination.totalCount,
@ -72,14 +62,20 @@ export function usePaginatedData() {
totalPages: result.pagination.totalPages,
pageCount: result.pagination.pageCount,
});
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setLoading(false);
}
}, [requestParams.page, requestParams.size, requestParams.orderFields, requestParams.orderTypes, requestParams.query]);
}, [
requestParams.page,
requestParams.size,
requestParams.orderFields,
requestParams.orderTypes,
requestParams.query,
]);
useEffect(() => {
const timer = setTimeout(() => {
@ -90,7 +86,7 @@ export function usePaginatedData() {
}, [fetchDataFromApi]);
const updatePagination = (updates: Partial<RequestParams>) => {
setRequestParams(prev => ({
setRequestParams((prev) => ({
...prev,
...updates,
}));
@ -98,16 +94,16 @@ export function usePaginatedData() {
// Create a combined refetch object that includes the setQuery function
const setQuery = (query: Record<string, string>) => {
setRequestParams(prev => ({
setRequestParams((prev) => ({
...prev,
query,
}));
};
const refetch = Object.assign(fetchDataFromApi, { setQuery });
// Combine request params and response metadata for backward compatibility
const pagination: Pagination = {
const pagination: PagePagination = {
...requestParams,
...responseMetadata,
};

View File

@ -1,55 +1,55 @@
// Language dictionary for the template component
const language = {
en: {
title: 'Data Management',
create: 'Create New',
view: 'View Item',
update: 'Update Item',
createNew: 'Create New Item',
back: 'Back',
cancel: 'Cancel',
submit: 'Submit',
noItemsFound: 'No items found',
previous: 'Previous',
next: 'Next',
page: 'Page',
of: 'of',
itemsPerPage: 'Items per page:',
sortBy: 'Sort by:',
loading: 'Loading...',
error: 'Error loading data:',
showing: 'Showing',
items: 'items',
total: 'Total',
title: "Data Management",
create: "Create New",
view: "View Item",
update: "Update Item",
createNew: "Create New Item",
back: "Back",
cancel: "Cancel",
submit: "Submit",
noItemsFound: "No items found",
previous: "Previous",
next: "Next",
page: "Page",
of: "of",
itemsPerPage: "Items per page:",
sortBy: "Sort by:",
loading: "Loading...",
error: "Error loading data:",
showing: "Showing",
items: "items",
total: "Total",
// Search related translations
search: 'Search',
searchPlaceholder: 'Enter search term...',
searchFields: 'Search in fields',
activeSearch: 'Active search',
clearSearch: 'Clear',
search: "Search",
searchPlaceholder: "Enter search term...",
searchFields: "Search in fields",
activeSearch: "Active search",
clearSearch: "Clear",
formLabels: {
title: 'Title',
description: 'Description',
status: 'Status',
createdAt: 'Created'
title: "Title",
description: "Description",
status: "Status",
createdAt: "Created",
},
status: {
active: 'Active',
inactive: 'Inactive'
active: "Active",
inactive: "Inactive",
},
buttons: {
view: 'View',
update: 'Update',
create: 'Create',
save: 'Save'
}
view: "View",
update: "Update",
create: "Create",
save: "Save",
},
},
// Add more languages as needed
};
export type LanguageKey = keyof typeof language;
export const getTranslation = (lang: LanguageKey = 'en') => {
export const getTranslation = (lang: LanguageKey = "en") => {
return language[lang] || language.en;
};

View File

@ -1,5 +1,7 @@
import { z } from "zod";
import { PagePagination } from "@/components/validations/list/paginations";
// Define the data schema using Zod
export const DataSchema = z.object({
id: z.string(),
@ -12,19 +14,6 @@ export const DataSchema = z.object({
export type DataType = z.infer<typeof DataSchema>;
// Define pagination interface
export interface Pagination {
page: number;
size: number;
totalCount: number;
allCount: number;
totalPages: number;
orderFields: string[];
orderTypes: string[];
pageCount: number;
query: Record<string, string>;
}
// Mock API function (replace with your actual API call)
export const fetchData = async ({
page = 1,
@ -49,7 +38,7 @@ export const fetchData = async ({
});
// Simulated API response
return new Promise<{ data: DataType[]; pagination: Pagination }>(
return new Promise<{ data: DataType[]; pagination: PagePagination }>(
(resolve) => {
setTimeout(() => {
// Generate mock data

View File

@ -0,0 +1,30 @@
// Define pagination interface
export interface PagePagination {
page: number;
size: number;
totalCount: number;
allCount: number;
totalPages: number;
orderFields: string[];
orderTypes: string[];
pageCount: number;
query: Record<string, string>;
}
// Define request parameters interface
export interface RequestParams {
page: number;
size: number;
orderFields: string[];
orderTypes: string[];
query: Record<string, string>;
}
// Define response metadata interface
export interface ResponseMetadata {
totalCount: number;
allCount: number;
totalPages: number;
pageCount: number;
}