Skip to content

Commit d4eb406

Browse files
Better date selection UI
1 parent 46b00f6 commit d4eb406

File tree

10 files changed

+121
-35
lines changed

10 files changed

+121
-35
lines changed

src/App.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function App() {
3333
const [loadedDays, setLoadedDays] = useState(0);
3434
const [categoryDataCache, setCategoryDataCache] = useState({});
3535
const [categoryLoadedDays, setCategoryLoadedDays] = useState({});
36+
const [selectedDateXPosition, setSelectedDateXPosition] = useState(null);
3637

3738
const activeRangeOption = useMemo(
3839
() => getTimeRangeOption(timeRangeId),
@@ -497,8 +498,14 @@ function App() {
497498
dateLabels={rangeDateLabels}
498499
availableDates={availableRangeDates}
499500
loading={dataLoading}
501+
onSelectedDatePosition={setSelectedDateXPosition}
502+
/>
503+
<NewListings
504+
data={filteredData}
505+
selectedDate={selectedDate}
506+
loading={dataLoading}
507+
selectedDateXPosition={selectedDateXPosition}
500508
/>
501-
<NewListings data={filteredData} selectedDate={selectedDate} loading={dataLoading} />
502509
</>
503510
) : (
504511
<>
@@ -518,12 +525,14 @@ function App() {
518525
dateLabels={rangeDateLabels}
519526
availableDates={availableRangeDates}
520527
loading={dataLoading}
528+
onSelectedDatePosition={setSelectedDateXPosition}
521529
/>
522530
<ModelListingsView
523531
data={filteredData}
524532
model={selectedModel}
525533
selectedDate={selectedDate}
526534
loading={dataLoading}
535+
selectedDateXPosition={selectedDateXPosition}
527536
/>
528537
</>
529538
)}

src/components/DetailChart.jsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export default function DetailChart({
3737
timeRangeOptions,
3838
dateLabels,
3939
availableDates,
40-
loading = false
40+
loading = false,
41+
onSelectedDatePosition
4142
}) {
4243
const { datasets, dates } = useMemo(() => {
4344
if (!data || data.length === 0 || !model) {
@@ -183,9 +184,7 @@ export default function DetailChart({
183184
}, [data, model, dateLabels, availableDates]);
184185

185186
return (
186-
<div className="detail-chart">
187-
<h2>{model} - Price History</h2>
188-
<PriceRangeChart
187+
<PriceRangeChart
189188
datasets={datasets}
190189
dates={dates}
191190
data={data}
@@ -198,7 +197,7 @@ export default function DetailChart({
198197
availableDates={availableDates}
199198
loading={loading}
200199
enableItemNavigation={false}
200+
onSelectedDatePosition={onSelectedDatePosition}
201201
/>
202-
</div>
203202
);
204203
}

src/components/ListingsTable.css

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
.listings-table {
2-
background: var(--bg-secondary);
3-
border-radius: 12px;
4-
padding: 1.5rem;
5-
box-shadow: 0 2px 6px var(--shadow-sm);
6-
}
7-
81
.listings-table__surface {
92
max-width: 100%;
103
overflow-x: auto;

src/components/ModelListingsView.css

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,24 @@
33
border-radius: 8px;
44
padding: 1.5rem;
55
box-shadow: 0 2px 4px var(--shadow-sm);
6-
margin-top: 2rem;
7-
overflow: hidden;
6+
margin-top: 1.0rem;
7+
overflow: visible;
8+
position: relative;
9+
border: 2px solid var(--primary-color);
10+
}
11+
12+
.model-listings-view__tail {
13+
position: absolute;
14+
top: -20px;
15+
transform: translateX(-50%);
16+
width: 0;
17+
height: 0;
18+
border-left: 16px solid transparent;
19+
border-right: 16px solid transparent;
20+
border-bottom: 20px solid var(--primary-color);
21+
z-index: 100;
22+
pointer-events: none;
23+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
824
}
925

1026
@media (max-width: 900px) {

src/components/ModelListingsView.jsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import React from 'react';
1+
import React, { useRef, useEffect, useState } from 'react';
22
import VehicleListingTabs from './VehicleListingTabs';
33
import { findNewListings, findListingsWithPriceChanges, findSoldListings, calculateDaysOnMarket } from '../services/dataLoader';
44
import './ModelListingsView.css';
55

6-
export default function ModelListingsView({ data, model, selectedDate, loading = false }) {
6+
export default function ModelListingsView({ data, model, selectedDate, loading = false, selectedDateXPosition = null }) {
7+
const wrapperRef = useRef(null);
8+
const [tailPosition, setTailPosition] = useState(null);
9+
10+
useEffect(() => {
11+
if (selectedDateXPosition !== null && wrapperRef.current) {
12+
const wrapperRect = wrapperRef.current.getBoundingClientRect();
13+
const relativeX = selectedDateXPosition - wrapperRect.left;
14+
setTailPosition(relativeX);
15+
} else {
16+
setTailPosition(null);
17+
}
18+
}, [selectedDateXPosition]);
719
if (!data || data.length === 0 || !model) {
820
if (loading) {
921
return <div className="loading">Loading model details...</div>;
@@ -62,7 +74,13 @@ export default function ModelListingsView({ data, model, selectedDate, loading =
6274
}));
6375

6476
return (
65-
<div className="model-listings-view">
77+
<div className="model-listings-view" ref={wrapperRef}>
78+
{tailPosition !== null && (
79+
<div
80+
className="model-listings-view__tail"
81+
style={{ left: `${tailPosition}px` }}
82+
/>
83+
)}
6684
<VehicleListingTabs
6785
newListings={newListings}
6886
changedListings={priceChangedListings}

src/components/NewListings.css

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,24 @@
33
border-radius: 8px;
44
padding: 1.5rem;
55
box-shadow: 0 2px 4px var(--shadow-sm);
6-
margin-top: 2rem;
7-
overflow: hidden;
6+
margin-top: 1.0rem;
7+
overflow: visible;
8+
position: relative;
9+
border: 2px solid var(--primary-color);
10+
}
11+
12+
.new-listings__tail {
13+
position: absolute;
14+
top: -26px;
15+
transform: translateX(-50%);
16+
width: 0;
17+
height: 0;
18+
border-left: 16px solid transparent;
19+
border-right: 16px solid transparent;
20+
border-bottom: 26px solid var(--primary-color);
21+
z-index: 100;
22+
pointer-events: none;
23+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
824
}
925

1026
.new-listings-header {

src/components/NewListings.jsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import VehicleListingTabs from './VehicleListingTabs';
33
import { findNewListings, findListingsWithPriceChanges, findSoldListings, calculateDaysOnMarket } from '../services/dataLoader';
44
import './NewListings.css';
55

6-
export default function NewListings({ data, selectedDate, loading = false }) {
6+
export default function NewListings({ data, selectedDate, loading = false, selectedDateXPosition = null }) {
77
const [selectedSource, setSelectedSource] = useState('all');
8+
const wrapperRef = useRef(null);
9+
const [tailPosition, setTailPosition] = useState(null);
10+
11+
useEffect(() => {
12+
if (selectedDateXPosition !== null && wrapperRef.current) {
13+
const wrapperRect = wrapperRef.current.getBoundingClientRect();
14+
const relativeX = selectedDateXPosition - wrapperRect.left;
15+
setTailPosition(relativeX);
16+
} else {
17+
setTailPosition(null);
18+
}
19+
}, [selectedDateXPosition]);
820

921
// Load source filter from URL on mount
1022
useEffect(() => {
@@ -114,7 +126,13 @@ export default function NewListings({ data, selectedDate, loading = false }) {
114126
);
115127

116128
return (
117-
<div className="new-listings">
129+
<div className="new-listings" ref={wrapperRef}>
130+
{tailPosition !== null && (
131+
<div
132+
className="new-listings__tail"
133+
style={{ left: `${tailPosition}px` }}
134+
/>
135+
)}
118136
<VehicleListingTabs
119137
newListings={filteredNewListings}
120138
changedListings={filteredChangedListings}

src/components/OverviewChart.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export default function OverviewChart({
5454
timeRangeOptions,
5555
dateLabels,
5656
availableDates,
57-
loading = false
57+
loading = false,
58+
onSelectedDatePosition
5859
}) {
5960
const { datasets, dates } = useMemo(() => {
6061
if (!data || data.length === 0) {
@@ -207,6 +208,7 @@ export default function OverviewChart({
207208
dateLabels={dateLabels}
208209
availableDates={availableDates}
209210
loading={loading}
211+
onSelectedDatePosition={onSelectedDatePosition}
210212
/>
211213
);
212214
}

src/components/PriceRangeChart.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,17 +262,18 @@
262262
}
263263

264264
.date-label:hover {
265-
color: #2563eb;
265+
color: var(--primary-color);
266266
text-decoration: none;
267267
}
268268

269269
.date-label.selected {
270-
color: #2563eb;
270+
color: var(--primary-color);
271271
font-weight: bold;
272272
}
273273

274274
.date-label.selected:hover {
275-
color: #1d4ed8;
275+
color: var(--primary-color);
276+
opacity: 0.8;
276277
}
277278

278279
.date-label.disabled {

src/components/PriceRangeChart.jsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export default function PriceRangeChart({
2222
dateLabels,
2323
availableDates,
2424
loading = false,
25-
enableItemNavigation = true
25+
enableItemNavigation = true,
26+
onSelectedDatePosition
2627
}) {
2728
const chartRef = useRef(null);
2829
const chartInstance = useRef(null);
2930
const labelsContainerRef = useRef(null);
3031
const dateLabelsContainerRef = useRef(null);
32+
const chartContainerRef = useRef(null);
3133
const [dotSizeMode, setDotSizeMode] = useState('stock');
3234
const [showItemLabels, setShowItemLabels] = useState(() => {
3335
if (typeof window === 'undefined') {
@@ -758,6 +760,22 @@ export default function PriceRangeChart({
758760
const xScale = chart.scales.x;
759761
const yScale = chart.scales.y;
760762

763+
// Find and report selected date position first
764+
if (selectedDate && onSelectedDatePosition && chartRef.current) {
765+
const selectedIndex = dates.findIndex((date, index) => {
766+
const grouped = datasets[0]?.groupedDatesSeries?.[index] || [date];
767+
return date === selectedDate || grouped.includes(selectedDate);
768+
});
769+
770+
if (selectedIndex >= 0) {
771+
const xPos = xScale.getPixelForValue(selectedIndex);
772+
const canvas = chartRef.current;
773+
const canvasRect = canvas.getBoundingClientRect();
774+
const viewportX = canvasRect.left + xPos;
775+
onSelectedDatePosition(viewportX);
776+
}
777+
}
778+
761779
const clickableDates = dates.filter((date, index) => {
762780
const axisLabel = formatAxisLabel(index);
763781
if (!axisLabel) {
@@ -800,6 +818,8 @@ export default function PriceRangeChart({
800818

801819
if (isSelected && hasData) {
802820
linkEl.classList.add('selected');
821+
// This position reporting is now done earlier in the afterDatasetsDraw hook
822+
// to ensure it happens on every chart render
803823
}
804824
if (!hasData) {
805825
linkEl.classList.add('disabled');
@@ -819,13 +839,7 @@ export default function PriceRangeChart({
819839
const textSpan = document.createElement('span');
820840
textSpan.textContent = axisLabel;
821841

822-
const chevron = document.createElement('span');
823-
chevron.textContent = '›';
824-
chevron.style.fontSize = '12px';
825-
chevron.style.opacity = '0.6';
826-
827842
linkEl.appendChild(textSpan);
828-
linkEl.appendChild(chevron);
829843

830844
if (hasData) {
831845
linkEl.style.cursor = 'pointer';
@@ -974,7 +988,7 @@ export default function PriceRangeChart({
974988
})}
975989
</div>
976990
)}
977-
<div className="chart-container">
991+
<div className="chart-container" ref={chartContainerRef}>
978992
{loading && (!datasets || datasets.length === 0) && (
979993
<div className="chart-loading">Loading price data...</div>
980994
)}

0 commit comments

Comments
 (0)