Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
/**
* Images Page - Built with TablePageTemplate
* Consistent with Keywords page layout, structure and design
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchTaskImages,
deleteTaskImage,
bulkDeleteTaskImages,
autoGenerateImages,
TaskImage,
TaskImageFilters,
} from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { FileIcon, DownloadIcon } from '../../icons';
import { createImagesPageConfig } from '../../config/pages/images.config';
export default function Images() {
const toast = useToast();
// Data state
const [images, setImages] = useState<TaskImage[]>([]);
const [loading, setLoading] = useState(true);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [imageTypeFilter, setImageTypeFilter] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// Sorting state
const [sortBy, setSortBy] = useState<string>('created_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Load images - wrapped in useCallback
const loadImages = useCallback(async () => {
setLoading(true);
setShowContent(false);
try {
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at';
const filters: TaskImageFilters = {
...(imageTypeFilter && { image_type: imageTypeFilter }),
...(statusFilter && { status: statusFilter }),
page: currentPage,
ordering,
};
// Note: TaskImages API doesn't support search by task title yet
// We'll filter client-side for now
const data = await fetchTaskImages(filters);
let filteredResults = data.results || [];
// Client-side search filter
if (searchTerm) {
filteredResults = filteredResults.filter(img =>
img.task_title?.toLowerCase().includes(searchTerm.toLowerCase())
);
}
setImages(filteredResults);
setTotalCount(filteredResults.length);
setTotalPages(Math.ceil(filteredResults.length / 10));
setTimeout(() => {
setShowContent(true);
setLoading(false);
}, 100);
} catch (error: any) {
console.error('Error loading images:', error);
toast.error(`Failed to load images: ${error.message}`);
setShowContent(true);
setLoading(false);
}
}, [currentPage, imageTypeFilter, statusFilter, sortBy, sortDirection, searchTerm]);
useEffect(() => {
loadImages();
}, [loadImages]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadImages();
} else {
setCurrentPage(1);
}
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadImages]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
setSortBy(field || 'created_at');
setSortDirection(direction);
setCurrentPage(1);
};
// Bulk export handler
const handleBulkExport = useCallback(async (ids: string[]) => {
try {
if (!ids || ids.length === 0) {
throw new Error('No records selected for export');
}
toast.info('Export functionality coming soon');
} catch (error: any) {
throw error;
}
}, []);
// Bulk action handler
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'generate_images') {
try {
const numIds = ids.map(id => parseInt(id));
// Note: autoGenerateImages expects task_ids, not image_ids
// This would need to be adjusted based on API design
toast.info(`Generate images for ${ids.length} items`);
// await autoGenerateImages(numIds);
} catch (error: any) {
toast.error(`Failed to generate images: ${error.message}`);
}
} else {
toast.info(`Bulk action "${action}" for ${ids.length} items`);
}
}, []);
// Create page config
const pageConfig = useMemo(() => {
return createImagesPageConfig({
searchTerm,
setSearchTerm,
imageTypeFilter,
setImageTypeFilter,
statusFilter,
setStatusFilter,
setCurrentPage,
});
}, [searchTerm, imageTypeFilter, statusFilter]);
// Calculate header metrics
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ images, totalCount }),
accentColor: metric.accentColor,
}));
}, [pageConfig?.headerMetrics, images, totalCount]);
return (
<TablePageTemplate
title="Task Images"
titleIcon={<FileIcon className="text-purple-500 size-5" />}
subtitle="Manage images for content tasks"
columns={pageConfig.columns}
data={images}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
image_type: imageTypeFilter,
status: statusFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'image_type') {
setImageTypeFilter(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
}
setCurrentPage(1);
}}
onDelete={async (id: number) => {
await deleteTaskImage(id);
loadImages();
}}
onBulkDelete={async (ids: number[]) => {
// Note: bulkDeleteTaskImages doesn't exist yet, using individual deletes
for (const id of ids) {
await deleteTaskImage(id);
}
loadImages();
return { deleted_count: ids.length };
}}
onBulkExport={handleBulkExport}
onBulkAction={handleBulkAction}
getItemDisplayName={(row: TaskImage) => row.task_title || `Image ${row.id}`}
onExport={async () => {
toast.info('Export functionality coming soon');
}}
onExportIcon={<DownloadIcon />}
selectionLabel="image"
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onFilterReset={() => {
setSearchTerm('');
setImageTypeFilter('');
setStatusFilter('');
setCurrentPage(1);
}}
/>
);
}