feat(T08): Bildretusche via Flux.1-Pro + Preisvergleich + Retouch UI with before/after slider
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { comparePrices, type PriceCompareResponse, type ComparableListing } from '@/lib/retouch';
|
||||
|
||||
interface PriceComparisonProps {
|
||||
vehicleId?: string;
|
||||
}
|
||||
|
||||
export function PriceComparison({ vehicleId }: PriceComparisonProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState<PriceCompareResponse | null>(null);
|
||||
const [inputVehicleId, setInputVehicleId] = useState(vehicleId || '');
|
||||
|
||||
const handleCompare = useCallback(async () => {
|
||||
const vid = inputVehicleId.trim();
|
||||
if (!vid) {
|
||||
setError('Please enter a vehicle ID');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await comparePrices(vid);
|
||||
setData(result);
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { error?: { message?: string } };
|
||||
setError(apiErr?.error?.message || 'Price comparison failed');
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [inputVehicleId]);
|
||||
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(price);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="price-comparison" className="space-y-4">
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-text mb-1">
|
||||
Vehicle ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputVehicleId}
|
||||
onChange={(e) => setInputVehicleId(e.target.value)}
|
||||
placeholder="Enter vehicle UUID"
|
||||
className="w-full px-3 py-2 border border-border rounded-lg text-text focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
data-testid="price-compare-vehicle-input"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCompare}
|
||||
loading={loading}
|
||||
data-testid="price-compare-button"
|
||||
>
|
||||
Compare Prices
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div data-testid="price-compare-error" className="p-4 bg-error/10 text-error rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div data-testid="price-compare-loading" className="text-center text-text-muted py-4">
|
||||
Searching comparable listings...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && !loading && (
|
||||
<div data-testid="price-compare-results" className="space-y-4">
|
||||
{/* Summary */}
|
||||
<div className="flex items-center justify-between p-4 bg-primary/5 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-text-muted">Comparable Listings</p>
|
||||
<p className="text-2xl font-bold text-text">{data.listing_count}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-text-muted">Average Price</p>
|
||||
<p className="text-2xl font-bold text-primary">
|
||||
{data.average_price !== null ? formatPrice(data.average_price) : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listings table */}
|
||||
{data.comparable_listings.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm border border-border rounded-lg">
|
||||
<thead className="bg-secondary/10">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-text font-medium">Title</th>
|
||||
<th className="px-4 py-2 text-right text-text font-medium">Price</th>
|
||||
<th className="px-4 py-2 text-right text-text font-medium">Mileage</th>
|
||||
<th className="px-4 py-2 text-left text-text font-medium">Location</th>
|
||||
<th className="px-4 py-2 text-left text-text font-medium">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.comparable_listings.map((listing: ComparableListing, idx: number) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="border-t border-border hover:bg-secondary/5"
|
||||
data-testid={`price-compare-row-${idx}`}
|
||||
>
|
||||
<td className="px-4 py-2 text-text">
|
||||
{listing.url ? (
|
||||
<a
|
||||
href={listing.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{listing.title}
|
||||
</a>
|
||||
) : (
|
||||
listing.title
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-text font-medium">
|
||||
{formatPrice(listing.price)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-text-muted">
|
||||
{listing.mileage_km !== null && listing.mileage_km !== undefined
|
||||
? `${listing.mileage_km.toLocaleString('de-DE')} km`
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-text-muted">
|
||||
{listing.location || '-'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-text-muted">
|
||||
{listing.source}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="price-compare-empty" className="text-center text-text-muted py-8">
|
||||
No comparable listings found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user