Skip to content

Commit 87f21b3

Browse files
author
Timothy Dodd
committed
feat: implement date range picker and time filter dropdown components
1 parent a4a9919 commit 87f21b3

15 files changed

Lines changed: 673 additions & 57 deletions

.claude/settings.local.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(ng generate:*)"
5+
],
6+
"deny": []
7+
}
8+
}

src/LogMkApi/Data/LogRepo.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,14 @@ public async Task<PagedResults<Log>> GetAll(int offset = 0,
8383
dynamicParameters.AddIfNotNull("offset", offset);
8484
dynamicParameters.AddIfNotNull("pageSize", pageSize);
8585
dynamicParameters.AddIfNotNull("dateStart", dateStart?.Date);
86-
dynamicParameters.AddIfNotNull("dateStartHour", dateStart?.Date.Hour);
87-
dynamicParameters.AddIfNotNull("dateEnd", dateStart?.Date);
86+
dynamicParameters.AddIfNotNull("dateStartHour", dateStart?.Hour);
87+
dynamicParameters.AddIfNotNull("dateEnd", dateEnd?.Date);
88+
dynamicParameters.AddIfNotNull("dateEndHour", dateEnd?.Hour);
8889

8990
dynamicParameters.AddIfNotNull("search", search);
9091

91-
whereBuilder.AppendAnd(dateStart, "l.LogDate >= @dateStart AND l.LogHour >= @dateStartHour");
92-
whereBuilder.AppendAnd(dateEnd, "l.LogDate <= @dateEnd");
92+
whereBuilder.AppendAnd(dateStart, "l.LogDate >= @dateStart AND (l.LogDate != @dateStart || (l.LogHour >= @dateStartHour))");
93+
whereBuilder.AppendAnd(dateEnd, "l.LogDate <= @dateEnd AND (l.LogDate != @dateEnd || (l.LogHour < @dateEndHour)) ");
9394

9495
var likeClause = new AndOrBuilder();
9596
likeClause.AppendOr(search, "l.Line LIKE @search ");
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<div class="date-range-picker">
2+
<div class="mb-3">
3+
<label class="form-label">Start Date & Time</label>
4+
<input
5+
type="datetime-local"
6+
class="form-control"
7+
step="3600"
8+
[value]="startDateString()"
9+
(input)="onStartDateChange($any($event.target).value || '')"
10+
placeholder="Select start date and time">
11+
</div>
12+
13+
<div class="mb-3">
14+
<label class="form-label">End Date & Time</label>
15+
<input
16+
type="datetime-local"
17+
class="form-control"
18+
step="3600"
19+
[value]="endDateString()"
20+
(input)="onEndDateChange($any($event.target).value || '')"
21+
placeholder="Select end date and time">
22+
</div>
23+
24+
<div class="d-flex gap-2 justify-content-end">
25+
<button
26+
type="button"
27+
class="btn btn-outline-secondary btn-sm"
28+
(click)="onCancel()">
29+
Cancel
30+
</button>
31+
<button
32+
type="button"
33+
class="btn btn-primary btn-sm"
34+
[disabled]="!canApply()"
35+
(click)="onApply()">
36+
Apply Range
37+
</button>
38+
</div>
39+
</div>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
.date-range-picker {
6+
padding: 1rem;
7+
background: var(--surface);
8+
border-radius: 0.5rem;
9+
border: 1px solid var(--border-color);
10+
min-width: 300px;
11+
}
12+
13+
.form-label {
14+
color: var(--on-surface);
15+
font-size: 0.875rem;
16+
font-weight: 500;
17+
margin-bottom: 0.5rem;
18+
}
19+
20+
.form-control {
21+
background: var(--input-bg);
22+
border: 1px solid var(--border-color);
23+
color: var(--on-surface);
24+
padding: 0.5rem 0.75rem;
25+
border-radius: 0.375rem;
26+
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
27+
28+
&:focus {
29+
border-color: var(--primary);
30+
box-shadow: 0 0 0 0.2rem var(--focus-ring);
31+
outline: none;
32+
}
33+
34+
/* Attempt to style datetime-local icons for dark theme */
35+
&[type="datetime-local"] {
36+
color-scheme: dark;
37+
38+
/* Webkit browsers (Chrome, Safari, Edge) */
39+
&::-webkit-calendar-picker-indicator {
40+
filter: invert(1) brightness(0.8);
41+
opacity: 0.7;
42+
cursor: pointer;
43+
}
44+
45+
&::-webkit-datetime-edit-fields-wrapper {
46+
color: var(--on-surface);
47+
}
48+
49+
&::-webkit-datetime-edit {
50+
color: var(--on-surface);
51+
}
52+
53+
&::-webkit-datetime-edit-text {
54+
color: var(--on-surface-variant);
55+
}
56+
57+
/* Firefox */
58+
&::-moz-color-swatch {
59+
border: none;
60+
}
61+
}
62+
}
63+
64+
.btn {
65+
font-size: 0.875rem;
66+
padding: 0.375rem 0.75rem;
67+
border-radius: 0.375rem;
68+
border: 1px solid transparent;
69+
cursor: pointer;
70+
transition: all 0.15s ease-in-out;
71+
72+
&:disabled {
73+
opacity: 0.6;
74+
cursor: not-allowed;
75+
}
76+
}
77+
78+
.btn-primary {
79+
background: var(--primary);
80+
color: var(--on-primary);
81+
border-color: var(--primary);
82+
83+
&:hover:not(:disabled) {
84+
background: color-mix(in srgb, var(--primary) 85%, white);
85+
border-color: color-mix(in srgb, var(--primary) 85%, white);
86+
}
87+
}
88+
89+
.btn-outline-secondary {
90+
background: transparent;
91+
color: var(--on-surface-variant);
92+
border-color: var(--border-color);
93+
94+
&:hover:not(:disabled) {
95+
background: var(--surface-variant);
96+
color: var(--on-surface);
97+
}
98+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { DateRangePickerComponent } from './date-range-picker.component';
4+
5+
describe('DateRangePickerComponent', () => {
6+
let component: DateRangePickerComponent;
7+
let fixture: ComponentFixture<DateRangePickerComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [DateRangePickerComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(DateRangePickerComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Component, output, input, computed, signal } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { format, startOfDay } from 'date-fns';
5+
6+
export interface DateRange {
7+
start: Date;
8+
end: Date;
9+
}
10+
11+
@Component({
12+
selector: 'app-date-range-picker',
13+
imports: [CommonModule, FormsModule],
14+
templateUrl: './date-range-picker.component.html',
15+
styleUrl: './date-range-picker.component.scss'
16+
})
17+
export class DateRangePickerComponent {
18+
// Inputs
19+
startDate = input<Date | null>(null);
20+
endDate = input<Date | null>(null);
21+
22+
// Outputs
23+
dateRangeChange = output<DateRange>();
24+
cancel = output<void>();
25+
26+
// Internal state
27+
private _startDateString = signal<string>('');
28+
private _endDateString = signal<string>('');
29+
30+
// Computed properties
31+
startDateString = computed(() => {
32+
const start = this.startDate();
33+
if (start) {
34+
return format(start, 'yyyy-MM-dd\'T\'HH:mm');
35+
}
36+
return this._startDateString();
37+
});
38+
39+
endDateString = computed(() => {
40+
const end = this.endDate();
41+
if (end) {
42+
return format(end, 'yyyy-MM-dd\'T\'HH:mm');
43+
}
44+
return this._endDateString();
45+
});
46+
47+
canApply = computed(() => {
48+
const startStr = this.startDateString();
49+
const endStr = this.endDateString();
50+
return startStr && endStr &&
51+
new Date(startStr) <= new Date(endStr);
52+
});
53+
54+
constructor() {
55+
// Initialize with current date/time if no values provided
56+
const now = new Date();
57+
const startOfToday = startOfDay(now);
58+
59+
this._startDateString.set(format(startOfToday, 'yyyy-MM-dd\'T\'HH:mm'));
60+
this._endDateString.set(format(now, 'yyyy-MM-dd\'T\'HH:mm'));
61+
}
62+
63+
onStartDateChange(value: string) {
64+
this._startDateString.set(value);
65+
}
66+
67+
onEndDateChange(value: string) {
68+
this._endDateString.set(value);
69+
}
70+
71+
onApply() {
72+
if (this.canApply()) {
73+
const start = new Date(this.startDateString());
74+
const end = new Date(this.endDateString());
75+
76+
this.dateRangeChange.emit({ start, end });
77+
}
78+
}
79+
80+
onCancel() {
81+
this.cancel.emit();
82+
}
83+
}

src/LogMkWeb/src/app/_components/dropdown/dropdown.component.scss

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@
121121
width: max-content;
122122
min-width: 100%;
123123
max-width: 400px;
124-
max-height: 240px;
125-
overflow: hidden;
124+
max-height: 500px;
125+
overflow: visible;
126126
z-index: 1000;
127127
}
128128

129129
.dropdown-items {
130-
max-height: 240px;
130+
max-height: 200px;
131131
overflow-y: auto;
132132
overflow-x: hidden;
133133

@@ -255,4 +255,17 @@
255255
box-shadow: 0 0 0 0.2rem var(--danger-bg);
256256
}
257257
}
258+
}
259+
260+
/* Custom content section */
261+
::ng-deep [slot="custom-content"] {
262+
border-top: 1px solid var(--border-color);
263+
margin-top: 0.25rem;
264+
padding-top: 0.25rem;
265+
}
266+
267+
.dropdown-panel [slot="custom-content"] {
268+
border-top: 1px solid var(--border-color);
269+
margin-top: 0.25rem;
270+
padding-top: 0.25rem;
258271
}

src/LogMkWeb/src/app/_components/dropdown/dropdown.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface DropdownItem {
8888
<div class="dropdown-item disabled">No options available</div>
8989
}
9090
</div>
91+
<ng-content select="[slot=custom-content]"></ng-content>
9192
</div>
9293
}
9394
</div>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<div class="time-filter-dropdown">
2+
<div
3+
class="dropdown-trigger"
4+
[class.focused]="isOpen()"
5+
[class.has-custom-range]="hasCustomRange()"
6+
(click)="toggle()"
7+
#trigger
8+
>
9+
<div class="dropdown-value">
10+
<span [class.placeholder]="!hasCustomRange() && !selectedFilter()">
11+
{{ displayValue() }}
12+
</span>
13+
</div>
14+
<lucide-icon
15+
name="chevron-down"
16+
size="16"
17+
class="dropdown-arrow"
18+
[class.rotated]="isOpen()"
19+
></lucide-icon>
20+
</div>
21+
22+
@if (isOpen()) {
23+
<div class="dropdown-panel" #dropdownPanel>
24+
<div class="dropdown-items">
25+
@for (filter of timeFilters(); track filter.value) {
26+
<div
27+
class="dropdown-item"
28+
[class.selected]="!hasCustomRange() && !customRangeSelected() && selectedFilter()?.value === filter.value"
29+
(click)="selectFilter(filter)"
30+
>
31+
{{ filter.label }}
32+
</div>
33+
}
34+
35+
<div class="dropdown-item custom-range-trigger"
36+
[class.selected]="customRangeSelected() || hasCustomRange()"
37+
(click)="selectCustomRange()">
38+
📅 Custom Date Range
39+
</div>
40+
</div>
41+
42+
@if (showDateRangePicker()) {
43+
<div class="custom-content">
44+
<app-date-range-picker
45+
[startDate]="logFilterState.customTimeRange()?.start || null"
46+
[endDate]="logFilterState.customTimeRange()?.end || null"
47+
(dateRangeChange)="onCustomDateRangeSelected($event)"
48+
(cancel)="hideCustomPicker()"
49+
></app-date-range-picker>
50+
</div>
51+
}
52+
</div>
53+
}
54+
</div>

0 commit comments

Comments
 (0)