Улучшены datetimepicker элементы
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { CalendarClock, ChevronDown, DatabaseZap, Search } from 'lucide-react';
|
import type { RefObject } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { CalendarDays, CalendarClock, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react';
|
||||||
import type { CurrentItem } from '../../../entities/current/types';
|
import type { CurrentItem } from '../../../entities/current/types';
|
||||||
import type { HistoryResponse } from '../../../entities/history/types';
|
import type { HistoryResponse } from '../../../entities/history/types';
|
||||||
import { HistoryChart } from '../../../features/history-chart/HistoryChart';
|
import { HistoryChart } from '../../../features/history-chart/HistoryChart';
|
||||||
@@ -27,6 +29,54 @@ type ArchiveViewProps = {
|
|||||||
onToggleTag: (tag: string) => void;
|
onToggleTag: (tag: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RangePart = 'from' | 'to';
|
||||||
|
type RangeInputPart = 'date' | 'time';
|
||||||
|
|
||||||
|
function getRangeInputPart(value: string, part: RangeInputPart): string {
|
||||||
|
const [date = '', time = ''] = value.split('T');
|
||||||
|
return part === 'date' ? date : time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRangeInputPart(value: string, part: RangeInputPart, nextValue: string): string {
|
||||||
|
const date = getRangeInputPart(value, 'date');
|
||||||
|
const time = getRangeInputPart(value, 'time');
|
||||||
|
return part === 'date' ? `${nextValue}T${time}` : `${date}T${nextValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInputPicker(input: HTMLInputElement | null): void {
|
||||||
|
input?.focus();
|
||||||
|
input?.showPicker?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateRangeFieldProps = {
|
||||||
|
icon: 'date' | 'time';
|
||||||
|
inputRef: RefObject<HTMLInputElement | null>;
|
||||||
|
type: 'date' | 'time';
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DateRangeField({ icon, inputRef, type, value, onChange }: DateRangeFieldProps) {
|
||||||
|
const Icon = icon === 'date' ? CalendarDays : Clock3;
|
||||||
|
const label = icon === 'date' ? 'Открыть календарь' : 'Открыть выбор времени';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="archive-date-field" onClick={() => openInputPicker(inputRef.current)}>
|
||||||
|
<button type="button" className="archive-date-picker-button" aria-label={label}>
|
||||||
|
<Icon size={16} />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
aria-label={label}
|
||||||
|
className={`archive-date-input archive-date-input--${type}`}
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ArchiveView({
|
export function ArchiveView({
|
||||||
getTagLabel,
|
getTagLabel,
|
||||||
history,
|
history,
|
||||||
@@ -49,6 +99,16 @@ export function ArchiveView({
|
|||||||
const [selectorOpen, setSelectorOpen] = useState(false);
|
const [selectorOpen, setSelectorOpen] = useState(false);
|
||||||
const selectedPreview = selectedTags.slice(0, 10);
|
const selectedPreview = selectedTags.slice(0, 10);
|
||||||
const hiddenSelectedCount = Math.max(0, selectedTags.length - selectedPreview.length);
|
const hiddenSelectedCount = Math.max(0, selectedTags.length - selectedPreview.length);
|
||||||
|
const fromDateRef = useRef<HTMLInputElement>(null);
|
||||||
|
const fromTimeRef = useRef<HTMLInputElement>(null);
|
||||||
|
const toDateRef = useRef<HTMLInputElement>(null);
|
||||||
|
const toTimeRef = useRef<HTMLInputElement>(null);
|
||||||
|
const updateRangePart = (rangePart: RangePart, inputPart: RangeInputPart, value: string) => {
|
||||||
|
onRangeChange({
|
||||||
|
...range,
|
||||||
|
[rangePart]: updateRangeInputPart(range[rangePart], inputPart, value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="chart-section chart-section--archive">
|
<section className="chart-section chart-section--archive">
|
||||||
@@ -148,22 +208,40 @@ export function ArchiveView({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<div className="archive-date-range">
|
||||||
С
|
<span>С</span>
|
||||||
<input
|
<DateRangeField
|
||||||
type="datetime-local"
|
icon="date"
|
||||||
value={range.from}
|
inputRef={fromDateRef}
|
||||||
onChange={(event) => onRangeChange({ ...range, from: event.target.value })}
|
type="date"
|
||||||
|
value={getRangeInputPart(range.from, 'date')}
|
||||||
|
onChange={(value) => updateRangePart('from', 'date', value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
<DateRangeField
|
||||||
<label>
|
icon="time"
|
||||||
По
|
inputRef={fromTimeRef}
|
||||||
<input
|
type="time"
|
||||||
type="datetime-local"
|
value={getRangeInputPart(range.from, 'time')}
|
||||||
value={range.to}
|
onChange={(value) => updateRangePart('from', 'time', value)}
|
||||||
onChange={(event) => onRangeChange({ ...range, to: event.target.value })}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
<div className="archive-date-range">
|
||||||
|
<span>По</span>
|
||||||
|
<DateRangeField
|
||||||
|
icon="date"
|
||||||
|
inputRef={toDateRef}
|
||||||
|
type="date"
|
||||||
|
value={getRangeInputPart(range.to, 'date')}
|
||||||
|
onChange={(value) => updateRangePart('to', 'date', value)}
|
||||||
|
/>
|
||||||
|
<DateRangeField
|
||||||
|
icon="time"
|
||||||
|
inputRef={toTimeRef}
|
||||||
|
type="time"
|
||||||
|
value={getRangeInputPart(range.to, 'time')}
|
||||||
|
onChange={(value) => updateRangePart('to', 'time', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedTags.length ? (
|
{selectedTags.length ? (
|
||||||
|
|||||||
@@ -181,6 +181,97 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-date-range {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input {
|
||||||
|
color-scheme: dark;
|
||||||
|
min-height: 36px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
padding: 0;
|
||||||
|
cursor: text;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input--date {
|
||||||
|
width: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input--time {
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-field {
|
||||||
|
min-height: 38px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: rgba(15, 23, 42, 0.72);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-picker-button {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(201, 122, 61, 0.34);
|
||||||
|
border-radius: calc(var(--radius) - 4px);
|
||||||
|
background: rgba(201, 122, 61, 0.14);
|
||||||
|
color: var(--accent-soft);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-picker-button:hover,
|
||||||
|
.archive-date-picker-button:focus-visible {
|
||||||
|
border-color: rgba(201, 122, 61, 0.62);
|
||||||
|
background: rgba(201, 122, 61, 0.24);
|
||||||
|
color: #fed7aa;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-field:hover,
|
||||||
|
.archive-date-field:focus-within {
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
background: rgba(201, 122, 61, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-picker-button svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input::-webkit-calendar-picker-indicator {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input::-webkit-datetime-edit,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-text,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-day-field,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-month-field,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-year-field,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-hour-field,
|
||||||
|
.archive-date-input::-webkit-datetime-edit-minute-field {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.tag-selector__header {
|
.tag-selector__header {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -191,4 +282,18 @@
|
|||||||
.tag-selector__tools {
|
.tag-selector__tools {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.archive-date-range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-date-input--date,
|
||||||
|
.archive-date-input--time {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user