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 React from "react";
import { DataType, Pagination } from './schema'; import { DataType } from "./schema";
import { getTranslation, LanguageKey } from './language'; import { getTranslation, LanguageKey } from "./language";
import { ActionButtonsComponent } from './ActionButtonsComponent'; import { ActionButtonsComponent } from "./ActionButtonsComponent";
import { SortingComponent } from './SortingComponent'; import { SortingComponent } from "./SortingComponent";
import { PaginationToolsComponent } from './PaginationToolsComponent'; import { PaginationToolsComponent } from "./PaginationToolsComponent";
import { PagePagination } from "@/components/validations/list/paginations";
interface DataCardProps { interface DataCardProps {
item: DataType; item: DataType;
@ -18,7 +17,7 @@ export function DataCard({
item, item,
onView, onView,
onUpdate, onUpdate,
lang = 'en' lang = "en",
}: DataCardProps) { }: DataCardProps) {
const t = getTranslation(lang); const t = getTranslation(lang);
@ -29,11 +28,18 @@ export function DataCard({
<h3 className="text-lg font-semibold">{item.title}</h3> <h3 className="text-lg font-semibold">{item.title}</h3>
<p className="text-gray-600 mt-1">{item.description}</p> <p className="text-gray-600 mt-1">{item.description}</p>
<div className="mt-2 flex items-center"> <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} {item.status}
</span> </span>
<span className="text-xs text-gray-500 ml-2"> <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> </span>
</div> </div>
</div> </div>
@ -58,10 +64,10 @@ export function DataCard({
interface ListInfoComponentProps { interface ListInfoComponentProps {
data: DataType[]; data: DataType[];
pagination: Pagination; pagination: PagePagination;
loading: boolean; loading: boolean;
error: Error | null; error: Error | null;
updatePagination: (updates: Partial<Pagination>) => void; updatePagination: (updates: Partial<PagePagination>) => void;
onCreateClick: () => void; onCreateClick: () => void;
onViewClick: (item: DataType) => void; onViewClick: (item: DataType) => void;
onUpdateClick: (item: DataType) => void; onUpdateClick: (item: DataType) => void;
@ -77,7 +83,7 @@ export function ListInfoComponent({
onCreateClick, onCreateClick,
onViewClick, onViewClick,
onUpdateClick, onUpdateClick,
lang = 'en' lang = "en",
}: ListInfoComponentProps) { }: ListInfoComponentProps) {
const t = getTranslation(lang); const t = getTranslation(lang);
@ -91,10 +97,7 @@ export function ListInfoComponent({
return ( return (
<> <>
<ActionButtonsComponent <ActionButtonsComponent onCreateClick={onCreateClick} lang={lang} />
onCreateClick={onCreateClick}
lang={lang}
/>
<SortingComponent <SortingComponent
pagination={pagination} pagination={pagination}
@ -114,7 +117,7 @@ export function ListInfoComponent({
</div> </div>
) : ( ) : (
<div className="grid gap-4"> <div className="grid gap-4">
{data.map(item => ( {data.map((item) => (
<DataCard <DataCard
key={item.id} key={item.id}
item={item} item={item}

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from "react";
import { Pagination } from './schema'; import { getTranslation, LanguageKey } from "./language";
import { getTranslation, LanguageKey } from './language'; import { PagePagination } from "@/components/validations/list/paginations";
interface PaginationToolsComponentProps { interface PaginationToolsComponentProps {
pagination: Pagination; pagination: PagePagination;
updatePagination: (updates: Partial<Pagination>) => void; updatePagination: (updates: Partial<PagePagination>) => void;
lang?: LanguageKey; lang?: LanguageKey;
} }
export function PaginationToolsComponent({ export function PaginationToolsComponent({
pagination, pagination,
updatePagination, updatePagination,
lang = 'en' lang = "en",
}: PaginationToolsComponentProps) { }: PaginationToolsComponentProps) {
const t = getTranslation(lang); const t = getTranslation(lang);
@ -53,23 +53,32 @@ export function PaginationToolsComponent({
{/* Items per page selector */} {/* Items per page selector */}
<div className="flex items-center space-x-2"> <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 <select
id="page-size" id="page-size"
value={pagination.size} value={pagination.size}
onChange={handleSizeChange} onChange={handleSizeChange}
className="border rounded px-2 py-1" className="border rounded px-2 py-1"
> >
{[5, 10, 20, 50].map(size => ( {[5, 10, 20, 50].map((size) => (
<option key={size} value={size}>{size}</option> <option key={size} value={size}>
{size}
</option>
))} ))}
</select> </select>
</div> </div>
{/* Pagination stats */} {/* Pagination stats */}
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
<div>{t.showing} {pagination.pageCount} {t.of} {pagination.totalCount} {t.items}</div> <div>
<div>{t.total}: {pagination.allCount} {t.items}</div> {t.showing} {pagination.pageCount} {t.of} {pagination.totalCount}{" "}
{t.items}
</div>
<div>
{t.total}: {pagination.allCount} {t.items}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,15 +1,18 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { DataSchema } from './schema'; import { DataSchema } from "./schema";
import { getTranslation, LanguageKey } from './language'; import { getTranslation, LanguageKey } from "./language";
interface SearchComponentProps { interface SearchComponentProps {
onSearch: (query: Record<string, string>) => void; onSearch: (query: Record<string, string>) => void;
lang?: LanguageKey; lang?: LanguageKey;
} }
export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps) { export function SearchComponent({
onSearch,
lang = "en",
}: SearchComponentProps) {
const t = getTranslation(lang); const t = getTranslation(lang);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState("");
const [activeFields, setActiveFields] = useState<string[]>([]); const [activeFields, setActiveFields] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<Record<string, string>>({}); const [searchQuery, setSearchQuery] = useState<Record<string, string>>({});
@ -22,7 +25,7 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
// Only add fields if we have a search value // Only add fields if we have a search value
if (searchValue) { if (searchValue) {
activeFields.forEach(field => { activeFields.forEach((field) => {
newQuery[field] = searchValue; newQuery[field] = searchValue;
}); });
} }
@ -57,7 +60,7 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
} }
const newQuery: Record<string, string> = {}; const newQuery: Record<string, string> = {};
activeFields.forEach(field => { activeFields.forEach((field) => {
newQuery[field] = value; newQuery[field] = value;
}); });
@ -66,9 +69,9 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
}; };
const toggleField = (field: string) => { const toggleField = (field: string) => {
setActiveFields(prev => { setActiveFields((prev) => {
if (prev.includes(field)) { if (prev.includes(field)) {
return prev.filter(f => f !== field); return prev.filter((f) => f !== field);
} else { } else {
return [...prev, field]; 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="bg-white p-4 rounded-lg shadow mb-4">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="search" className="block text-sm font-medium mb-1"> <label htmlFor="search" className="block text-sm font-medium mb-1">
{t.search || 'Search'} {t.search || "Search"}
</label> </label>
<input <input
type="text" type="text"
id="search" id="search"
value={searchValue} value={searchValue}
onChange={handleSearchChange} 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" className="w-full p-2 border rounded focus:ring-blue-500 focus:border-blue-500"
/> />
</div> </div>
<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"> <div className="flex flex-wrap gap-2">
{Object.keys(DataSchema.shape).map(field => ( {Object.keys(DataSchema.shape).map((field) => (
<button <button
key={field} key={field}
onClick={() => toggleField(field)} onClick={() => toggleField(field)}
className={`px-3 py-1 text-sm rounded ${ className={`px-3 py-1 text-sm rounded ${
activeFields.includes(field) activeFields.includes(field)
? 'bg-blue-500 text-white' ? "bg-blue-500 text-white"
: 'bg-gray-200 text-gray-700' : "bg-gray-200 text-gray-700"
}`} }`}
> >
{field} {field}
@ -112,10 +117,15 @@ export function SearchComponent({ onSearch, lang = 'en' }: SearchComponentProps)
{Object.keys(searchQuery).length > 0 && ( {Object.keys(searchQuery).length > 0 && (
<div className="mt-3 p-2 bg-gray-100 rounded"> <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"> <div className="flex flex-wrap gap-2">
{Object.entries(searchQuery).map(([field, value]) => ( {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} {field}: {value}
<button <button
onClick={() => toggleField(field)} onClick={() => toggleField(field)}

View File

@ -1,17 +1,18 @@
import React from 'react'; import React from "react";
import { Pagination, DataSchema } from './schema'; import { DataSchema } from "./schema";
import { getTranslation, LanguageKey } from './language'; import { getTranslation, LanguageKey } from "./language";
import { PagePagination } from "@/components/validations/list/paginations";
interface SortingComponentProps { interface SortingComponentProps {
pagination: Pagination; pagination: PagePagination;
updatePagination: (updates: Partial<Pagination>) => void; updatePagination: (updates: Partial<PagePagination>) => void;
lang?: LanguageKey; lang?: LanguageKey;
} }
export function SortingComponent({ export function SortingComponent({
pagination, pagination,
updatePagination, updatePagination,
lang = 'en' lang = "en",
}: SortingComponentProps) { }: SortingComponentProps) {
const t = getTranslation(lang); const t = getTranslation(lang);
@ -26,10 +27,10 @@ export function SortingComponent({
if (fieldIndex === -1) { if (fieldIndex === -1) {
// Field is not being sorted yet - add it with 'asc' direction // Field is not being sorted yet - add it with 'asc' direction
newOrderFields.push(field); newOrderFields.push(field);
newOrderTypes.push('asc'); newOrderTypes.push("asc");
} else if (pagination.orderTypes[fieldIndex] === 'asc') { } else if (pagination.orderTypes[fieldIndex] === "asc") {
// Field is being sorted ascending - change to descending // Field is being sorted ascending - change to descending
newOrderTypes[fieldIndex] = 'desc'; newOrderTypes[fieldIndex] = "desc";
} else { } else {
// Field is being sorted descending - remove it from sorting // Field is being sorted descending - remove it from sorting
newOrderFields.splice(fieldIndex, 1); newOrderFields.splice(fieldIndex, 1);
@ -48,22 +49,26 @@ export function SortingComponent({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<label className="text-sm font-medium">{t.sortBy}</label> <label className="text-sm font-medium">{t.sortBy}</label>
<div className="flex flex-wrap gap-2"> <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 // Find if this field is in the orderFields array
const fieldIndex = pagination.orderFields.indexOf(field); const fieldIndex = pagination.orderFields.indexOf(field);
const isActive = fieldIndex !== -1; const isActive = fieldIndex !== -1;
const direction = isActive ? pagination.orderTypes[fieldIndex] : null; const direction = isActive
? pagination.orderTypes[fieldIndex]
: null;
return ( return (
<button <button
key={field} key={field}
onClick={() => handleSortChange(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} {field}
{isActive && ( {isActive && (
<span className="ml-1"> <span className="ml-1">
{direction === 'asc' ? '↑' : '↓'} {direction === "asc" ? "↑" : "↓"}
</span> </span>
)} )}
</button> </button>

View File

@ -1,22 +1,10 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from "react";
import { DataType, Pagination, fetchData, DataSchema } from './schema'; import { DataType, fetchData, DataSchema } from "./schema";
import {
// Define request parameters interface PagePagination,
interface RequestParams { RequestParams,
page: number; ResponseMetadata,
size: number; } from "@/components/validations/list/paginations";
orderFields: string[];
orderTypes: string[];
query: Record<string, string>;
}
// Define response metadata interface
interface ResponseMetadata {
totalCount: number;
allCount: number;
totalPages: number;
pageCount: number;
}
// Custom hook for pagination and data fetching // Custom hook for pagination and data fetching
export function usePaginatedData() { export function usePaginatedData() {
@ -26,8 +14,8 @@ export function usePaginatedData() {
const [requestParams, setRequestParams] = useState<RequestParams>({ const [requestParams, setRequestParams] = useState<RequestParams>({
page: 1, page: 1,
size: 10, size: 10,
orderFields: ['createdAt'], orderFields: ["createdAt"],
orderTypes: ['desc'], orderTypes: ["desc"],
query: {}, query: {},
}); });
@ -54,14 +42,16 @@ export function usePaginatedData() {
}); });
// Validate data with Zod // Validate data with Zod
const validatedData = result.data.map(item => { const validatedData = result.data
try { .map((item) => {
return DataSchema.parse(item); try {
} catch (err) { return DataSchema.parse(item);
console.error('Validation error for item:', item, err); } catch (err) {
return null; console.error("Validation error for item:", item, err);
} return null;
}).filter(Boolean) as DataType[]; }
})
.filter(Boolean) as DataType[];
setData(validatedData); setData(validatedData);
@ -75,11 +65,17 @@ export function usePaginatedData() {
setError(null); setError(null);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error')); setError(err instanceof Error ? err : new Error("Unknown error"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [requestParams.page, requestParams.size, requestParams.orderFields, requestParams.orderTypes, requestParams.query]); }, [
requestParams.page,
requestParams.size,
requestParams.orderFields,
requestParams.orderTypes,
requestParams.query,
]);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -90,7 +86,7 @@ export function usePaginatedData() {
}, [fetchDataFromApi]); }, [fetchDataFromApi]);
const updatePagination = (updates: Partial<RequestParams>) => { const updatePagination = (updates: Partial<RequestParams>) => {
setRequestParams(prev => ({ setRequestParams((prev) => ({
...prev, ...prev,
...updates, ...updates,
})); }));
@ -98,7 +94,7 @@ export function usePaginatedData() {
// Create a combined refetch object that includes the setQuery function // Create a combined refetch object that includes the setQuery function
const setQuery = (query: Record<string, string>) => { const setQuery = (query: Record<string, string>) => {
setRequestParams(prev => ({ setRequestParams((prev) => ({
...prev, ...prev,
query, query,
})); }));
@ -107,7 +103,7 @@ export function usePaginatedData() {
const refetch = Object.assign(fetchDataFromApi, { setQuery }); const refetch = Object.assign(fetchDataFromApi, { setQuery });
// Combine request params and response metadata for backward compatibility // Combine request params and response metadata for backward compatibility
const pagination: Pagination = { const pagination: PagePagination = {
...requestParams, ...requestParams,
...responseMetadata, ...responseMetadata,
}; };

View File

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

View File

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