Compare commits

...

4 commits

4 changed files with 191 additions and 62 deletions

Binary file not shown.

View file

@ -0,0 +1,132 @@
'use client';
import React, { Suspense } from 'react';
import { useParams } from 'next/navigation';
import { ItemsList } from '@/components/ItemsList';
// Loading component for better UX
function LoadingSpinner() {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex flex-col items-center space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<span className="text-gray-600 text-sm">Loading items...</span>
</div>
</div>
);
}
// Error boundary component
function ErrorFallback({ error, resetError }: { error: Error; resetError: () => void }) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] p-8">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md w-full">
<h2 className="text-red-800 text-lg font-semibold mb-2">
Something went wrong
</h2>
<p className="text-red-600 text-sm mb-4">
{error.message || 'An unexpected error occurred while loading the items.'}
</p>
<div className="flex space-x-3">
<button
onClick={resetError}
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors text-sm"
>
Try again
</button>
<button
onClick={() => window.location.reload()}
className="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors text-sm"
>
Reload page
</button>
</div>
</div>
</div>
);
}
// Simple error boundary implementation
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: (error: Error, resetError: () => void) => React.ReactNode },
{ hasError: boolean; error?: Error }
> {
constructor(props: { children: React.ReactNode; fallback: (error: Error, resetError: () => void) => React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, () => {
this.setState({ hasError: false, error: undefined });
});
}
return this.props.children;
}
}
export default function UrlPage() {
const params = useParams();
const encodedUrlParam = params.encodedUrl || '';
// Handle both string and string[] cases
const encodedUrl = Array.isArray(encodedUrlParam) ? encodedUrlParam[0] : encodedUrlParam;
const decodedUrl = decodeURIComponent(encodedUrl);
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900">
CompareWare
</h1>
</div>
<div className="flex items-center space-x-4">
<div className="text-sm text-gray-500">
Item Comparison Tool
</div>
</div>
</div>
</div>
</header>
{/* Main content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{`Items Comparison for "${decodedUrl}"`}
</h2>
</div>
{/* Items list with error boundary and suspense */}
<ErrorBoundary fallback={(error, resetError) => <ErrorFallback error={error} resetError={resetError} />}>
<Suspense fallback={<LoadingSpinner />}>
<ItemsList url={decodedUrl} />
</Suspense>
</ErrorBoundary>
</main>
{/* Footer */}
<footer className="bg-white border-t mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center text-gray-500 text-sm">
<p>&copy; 2025 CompareWare.</p>
</div>
</div>
</footer>
</div>
);
}

View file

@ -26,65 +26,67 @@ export function EditableCell({
className = '' className = ''
}: EditableCellProps) { }: EditableCellProps) {
const [localValue, setLocalValue] = useState(value); const [localValue, setLocalValue] = useState(value);
const [isExternalUpdate, setIsExternalUpdate] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const lastExternalValueRef = useRef(value);
const isUserInputRef = useRef(false);
// Update local value when prop changes (including external updates) // Update local value when prop changes (only for genuine external updates)
useEffect(() => { useEffect(() => {
if (value !== localValue) { // Only update if this is a genuine external change (not from user input)
if (value !== lastExternalValueRef.current && !isUserInputRef.current) {
console.log('External value change detected for cell:', cellKey, value);
setLocalValue(value); setLocalValue(value);
setIsExternalUpdate(true);
} }
}, [value, localValue]); lastExternalValueRef.current = value;
// Reset external update flag after a short delay // Reset user input flag after a short delay
useEffect(() => { if (isUserInputRef.current) {
if (isExternalUpdate) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsExternalUpdate(false); isUserInputRef.current = false;
}, 100); }, 100);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isExternalUpdate]); }, [value, cellKey]);
// Handle focus when this cell becomes focused // Handle focus when this cell becomes focused
useEffect(() => { useEffect(() => {
if (focusedCell === cellKey) { if (focusedCell === cellKey) {
const element = inputType === 'textarea' ? textareaRef.current : inputRef.current; const element = inputType === 'textarea' ? textareaRef.current : inputRef.current;
if (element) { if (element && document.activeElement !== element) {
element.focus(); element.focus();
// Select all text when focusing, but not during external updates // Only select text if this is not from user input
if (!isExternalUpdate) { if (!isUserInputRef.current) {
element.select(); element.select();
} }
} }
} }
}, [focusedCell, cellKey, inputType, isExternalUpdate]); }, [focusedCell, cellKey, inputType]);
const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = e.target.value; const newValue = e.target.value;
console.log('Input event:', newValue); console.log('Input event for cell:', cellKey, newValue);
isUserInputRef.current = true; // Mark as user input
setLocalValue(newValue); setLocalValue(newValue);
}, []); }, [cellKey]);
const commitInput = useCallback(() => { const commitInput = useCallback(() => {
console.log('Committing input:', localValue); console.log('Committing input for cell:', cellKey, localValue);
onInput(localValue); onInput(localValue);
}, [localValue, onInput]); }, [localValue, onInput, cellKey]);
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
console.log('Focus gained for key:', cellKey); console.log('Focus gained for cell:', cellKey);
setFocusedCell(cellKey); setFocusedCell(cellKey);
onFocus?.(); onFocus?.();
}, [cellKey, setFocusedCell, onFocus]); }, [cellKey, setFocusedCell, onFocus]);
const handleBlur = useCallback(() => { const handleBlur = useCallback(() => {
console.log('Focus lost'); console.log('Focus lost for cell:', cellKey);
setFocusedCell(null); setFocusedCell(null);
commitInput(); commitInput();
onBlur?.(); onBlur?.();
}, [setFocusedCell, commitInput, onBlur]); }, [setFocusedCell, commitInput, onBlur, cellKey]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => { const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && inputType === 'text') { if (e.key === 'Enter' && inputType === 'text') {

View file

@ -274,13 +274,14 @@ export function ItemsList({ url }: ItemsListProps) {
setPropertyLabels(prev => ({ ...prev, ...labels })); setPropertyLabels(prev => ({ ...prev, ...labels }));
}); });
} }
// Set initial focus
if (loadedItems.length > 0 && !focusedItemId) {
setFocusedItemId(loadedItems[0].id);
}
} }
}, [itemsData, focusedItemId]); }, [itemsData]);
// Reset focus state when URL changes
useEffect(() => {
setFocusedCell(null);
setFocusedItemId(null);
}, [url]);
// Mutations // Mutations
const saveItemMutation = useMutation({ const saveItemMutation = useMutation({
@ -568,48 +569,42 @@ export function ItemsList({ url }: ItemsListProps) {
<div className="p-4"> <div className="p-4">
<h1 className="text-2xl font-bold mb-4">Items List</h1> <h1 className="text-2xl font-bold mb-4">Items List</h1>
<div className="overflow-x-auto"> <div className="table-container">
<table className="min-w-full border-collapse border border-gray-300"> <table>
<thead>
<tr>
<th className="border border-gray-300 p-2 bg-gray-100">Property</th>
{items.map((item, index) => (
<th key={item.id} className="border border-gray-300 p-2 bg-gray-100 relative">
{item.name || `Item ${index + 1}`}
<button
onClick={() => removeItem(index)}
className="absolute top-1 right-1 w-5 h-5 bg-red-500 text-white rounded-full text-xs hover:bg-red-600"
title="Delete item"
>
×
</button>
</th>
))}
</tr>
</thead>
<tbody> <tbody>
{/* Name row */} {/* Name row */}
<tr> <tr>
<td className="border border-gray-300 p-2 font-medium">Name</td> <td className="font-medium">Name</td>
{items.map((item, index) => ( {items.map((item, index) => (
<td key={`name-${item.id}`} className="border border-gray-300 p-2"> <td key={`name-${item.id}`}>
<TypeaheadInput <div className="flex items-center gap-2">
value={item.name} <div className="flex-1">
onInput={(value) => updateItem(index, 'name', value)} <TypeaheadInput
onSelect={(suggestion) => handleWikidataSelect(suggestion, item.id)} value={item.name}
fetchSuggestions={fetchWikidataSuggestions} onInput={(value) => updateItem(index, 'name', value)}
onFocus={() => setFocusedItemId(item.id)} onSelect={(suggestion) => handleWikidataSelect(suggestion, item.id)}
placeholder="Enter item name" fetchSuggestions={fetchWikidataSuggestions}
/> onFocus={() => setFocusedItemId(item.id)}
placeholder="Enter item name"
/>
</div>
<button
onClick={() => removeItem(index)}
className="delete-button item-delete"
title="Delete item"
>
×
</button>
</div>
</td> </td>
))} ))}
</tr> </tr>
{/* Description row */} {/* Description row */}
<tr> <tr>
<td className="border border-gray-300 p-2 font-medium">Description</td> <td className="font-medium">Description</td>
{items.map((item, index) => ( {items.map((item, index) => (
<td key={`description-${item.id}`} className="border border-gray-300 p-2"> <td key={`description-${item.id}`}>
<EditableCell <EditableCell
value={item.description} value={item.description}
onInput={(value) => updateItem(index, 'description', value)} onInput={(value) => updateItem(index, 'description', value)}
@ -627,18 +622,18 @@ export function ItemsList({ url }: ItemsListProps) {
const propertyLabel = propertyLabels[property] || property; const propertyLabel = propertyLabels[property] || property;
return ( return (
<tr key={property}> <tr key={property}>
<td className="border border-gray-300 p-2 font-medium relative"> <td className="font-medium relative">
{propertyLabel} {propertyLabel}
<button <button
onClick={() => removeProperty(property)} onClick={() => removeProperty(property)}
className="absolute top-1 right-1 w-5 h-5 bg-red-500 text-white rounded-full text-xs hover:bg-red-600" className="delete-button property-delete"
title="Delete property" title="Delete property"
> >
× ×
</button> </button>
</td> </td>
{items.map((item, index) => ( {items.map((item, index) => (
<td key={`${property}-${item.id}`} className="border border-gray-300 p-2"> <td key={`${property}-${item.id}`}>
<EditableCell <EditableCell
value={item.customProperties[property] || ''} value={item.customProperties[property] || ''}
onInput={(value) => updateItem(index, property, value)} onInput={(value) => updateItem(index, property, value)}