Compare commits
4 commits
95adee9710
...
8c9b7d84ca
Author | SHA1 | Date | |
---|---|---|---|
8c9b7d84ca | |||
5f5d8f00d9 | |||
afc70a56ba | |||
109377f083 |
4 changed files with 191 additions and 62 deletions
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
132
src/app/[encodedUrl]/page.tsx
Normal file
132
src/app/[encodedUrl]/page.tsx
Normal 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>© 2025 CompareWare.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -26,65 +26,67 @@ export function EditableCell({
|
|||
className = ''
|
||||
}: EditableCellProps) {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
const [isExternalUpdate, setIsExternalUpdate] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(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(() => {
|
||||
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);
|
||||
setIsExternalUpdate(true);
|
||||
}
|
||||
}, [value, localValue]);
|
||||
lastExternalValueRef.current = value;
|
||||
|
||||
// Reset external update flag after a short delay
|
||||
useEffect(() => {
|
||||
if (isExternalUpdate) {
|
||||
// Reset user input flag after a short delay
|
||||
if (isUserInputRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsExternalUpdate(false);
|
||||
isUserInputRef.current = false;
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isExternalUpdate]);
|
||||
}, [value, cellKey]);
|
||||
|
||||
// Handle focus when this cell becomes focused
|
||||
useEffect(() => {
|
||||
if (focusedCell === cellKey) {
|
||||
const element = inputType === 'textarea' ? textareaRef.current : inputRef.current;
|
||||
if (element) {
|
||||
if (element && document.activeElement !== element) {
|
||||
element.focus();
|
||||
// Select all text when focusing, but not during external updates
|
||||
if (!isExternalUpdate) {
|
||||
// Only select text if this is not from user input
|
||||
if (!isUserInputRef.current) {
|
||||
element.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [focusedCell, cellKey, inputType, isExternalUpdate]);
|
||||
}, [focusedCell, cellKey, inputType]);
|
||||
|
||||
const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
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);
|
||||
}, []);
|
||||
}, [cellKey]);
|
||||
|
||||
const commitInput = useCallback(() => {
|
||||
console.log('Committing input:', localValue);
|
||||
console.log('Committing input for cell:', cellKey, localValue);
|
||||
onInput(localValue);
|
||||
}, [localValue, onInput]);
|
||||
}, [localValue, onInput, cellKey]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
console.log('Focus gained for key:', cellKey);
|
||||
console.log('Focus gained for cell:', cellKey);
|
||||
setFocusedCell(cellKey);
|
||||
onFocus?.();
|
||||
}, [cellKey, setFocusedCell, onFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
console.log('Focus lost');
|
||||
console.log('Focus lost for cell:', cellKey);
|
||||
setFocusedCell(null);
|
||||
commitInput();
|
||||
onBlur?.();
|
||||
}, [setFocusedCell, commitInput, onBlur]);
|
||||
}, [setFocusedCell, commitInput, onBlur, cellKey]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && inputType === 'text') {
|
||||
|
|
|
@ -274,13 +274,14 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
setPropertyLabels(prev => ({ ...prev, ...labels }));
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [itemsData]);
|
||||
|
||||
// Set initial focus
|
||||
if (loadedItems.length > 0 && !focusedItemId) {
|
||||
setFocusedItemId(loadedItems[0].id);
|
||||
}
|
||||
}
|
||||
}, [itemsData, focusedItemId]);
|
||||
// Reset focus state when URL changes
|
||||
useEffect(() => {
|
||||
setFocusedCell(null);
|
||||
setFocusedItemId(null);
|
||||
}, [url]);
|
||||
|
||||
// Mutations
|
||||
const saveItemMutation = useMutation({
|
||||
|
@ -568,31 +569,16 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Items List</h1>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border-collapse border border-gray-300">
|
||||
<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>
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<tbody>
|
||||
{/* Name row */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 p-2 font-medium">Name</td>
|
||||
<td className="font-medium">Name</td>
|
||||
{items.map((item, index) => (
|
||||
<td key={`name-${item.id}`} className="border border-gray-300 p-2">
|
||||
<td key={`name-${item.id}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<TypeaheadInput
|
||||
value={item.name}
|
||||
onInput={(value) => updateItem(index, 'name', value)}
|
||||
|
@ -601,15 +587,24 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
onFocus={() => setFocusedItemId(item.id)}
|
||||
placeholder="Enter item name"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeItem(index)}
|
||||
className="delete-button item-delete"
|
||||
title="Delete item"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{/* Description row */}
|
||||
<tr>
|
||||
<td className="border border-gray-300 p-2 font-medium">Description</td>
|
||||
<td className="font-medium">Description</td>
|
||||
{items.map((item, index) => (
|
||||
<td key={`description-${item.id}`} className="border border-gray-300 p-2">
|
||||
<td key={`description-${item.id}`}>
|
||||
<EditableCell
|
||||
value={item.description}
|
||||
onInput={(value) => updateItem(index, 'description', value)}
|
||||
|
@ -627,18 +622,18 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
const propertyLabel = propertyLabels[property] || property;
|
||||
return (
|
||||
<tr key={property}>
|
||||
<td className="border border-gray-300 p-2 font-medium relative">
|
||||
<td className="font-medium relative">
|
||||
{propertyLabel}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
{items.map((item, index) => (
|
||||
<td key={`${property}-${item.id}`} className="border border-gray-300 p-2">
|
||||
<td key={`${property}-${item.id}`}>
|
||||
<EditableCell
|
||||
value={item.customProperties[property] || ''}
|
||||
onInput={(value) => updateItem(index, property, value)}
|
||||
|
|
Loading…
Add table
Reference in a new issue