better date handling

This commit is contained in:
Josh Hawkins 2024-09-18 11:47:48 -05:00
parent eb6d2c8983
commit 89f8f69948
3 changed files with 129 additions and 31 deletions

View File

@ -5,8 +5,8 @@ import {
LuImage, LuImage,
LuChevronDown, LuChevronDown,
LuChevronUp, LuChevronUp,
LuSave,
LuTrash2, LuTrash2,
LuStar,
} from "react-icons/lu"; } from "react-icons/lu";
import { import {
FilterType, FilterType,
@ -28,23 +28,8 @@ import { TooltipPortal } from "@radix-ui/react-tooltip";
import { usePersistence } from "@/hooks/use-persistence"; import { usePersistence } from "@/hooks/use-persistence";
import { SaveSearchDialog } from "./SaveSearchDialog"; import { SaveSearchDialog } from "./SaveSearchDialog";
import { DeleteSearchDialog } from "./DeleteSearchDialog"; import { DeleteSearchDialog } from "./DeleteSearchDialog";
import { convertLocalDateToTimestamp } from "@/utils/dateUtil";
const convertMMDDYYToTimestamp = (dateString: string): number => { import { toast } from "sonner";
const match = dateString.match(/^(\d{2})(\d{2})(\d{2})$/);
if (!match) return 0;
const [, month, day, year] = match;
const date = new Date(`20${year}-${month}-${day}T00:00:00Z`);
return date.getTime();
};
const emptyObject = Object.freeze([
{
name: "",
search: "",
filter: undefined,
},
]) as SavedSearchQuery[];
type InputWithTagsProps = { type InputWithTagsProps = {
filters: SearchFilter; filters: SearchFilter;
@ -76,7 +61,7 @@ export default function InputWithTags({
const [searchHistory, setSearchHistory, searchHistoryLoaded] = usePersistence< const [searchHistory, setSearchHistory, searchHistoryLoaded] = usePersistence<
SavedSearchQuery[] SavedSearchQuery[]
>("frigate-search-history", emptyObject); >("frigate-search-history");
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
@ -198,9 +183,39 @@ export default function InputWithTags({
switch (type) { switch (type) {
case "before": case "before":
case "after": case "after":
timestamp = convertMMDDYYToTimestamp(value); timestamp = convertLocalDateToTimestamp(value);
if (timestamp > 0) { if (timestamp > 0) {
newFilters[type] = timestamp; // Check for conflicts with existing before/after filters
if (
type === "before" &&
filters.after &&
timestamp <= filters.after * 1000
) {
toast.error(
"The 'before' date must be later than the 'after' date.",
{
position: "top-center",
},
);
return;
}
if (
type === "after" &&
filters.before &&
timestamp >= filters.before * 1000
) {
toast.error(
"The 'after' date must be earlier than the 'before' date.",
{
position: "top-center",
},
);
return;
}
if (type === "before") {
timestamp -= 1;
}
newFilters[type] = timestamp / 1000;
} }
break; break;
case "search_type": case "search_type":
@ -247,7 +262,7 @@ export default function InputWithTags({
trimmedValue, trimmedValue,
) || ) ||
((filterType === "before" || filterType === "after") && ((filterType === "before" || filterType === "after") &&
trimmedValue.match(/^\d{6}$/)) trimmedValue.match(/^\d{8}$/))
) { ) {
createFilter(filterType, trimmedValue); createFilter(filterType, trimmedValue);
setInputValue((prev) => { setInputValue((prev) => {
@ -295,7 +310,7 @@ export default function InputWithTags({
if (filterType === "before" || filterType === "after") { if (filterType === "before" || filterType === "after") {
// For before and after, we don't need to update suggestions // For before and after, we don't need to update suggestions
if (filterValue.match(/^\d{6}$/)) { if (filterValue.match(/^\d{8}$/)) {
handleFilterCreation(filterType, filterValue); handleFilterCreation(filterType, filterValue);
} }
} else { } else {
@ -414,7 +429,11 @@ export default function InputWithTags({
return ( return (
<> <>
<Command shouldFilter={false} ref={commandRef} className="rounded-md"> <Command
shouldFilter={false}
ref={commandRef}
className="rounded-md border"
>
<div className="relative"> <div className="relative">
<CommandInput <CommandInput
ref={inputRef} ref={inputRef}
@ -444,7 +463,7 @@ export default function InputWithTags({
{(search || Object.keys(filters).length > 0) && ( {(search || Object.keys(filters).length > 0) && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<LuSave <LuStar
className="size-4 cursor-pointer text-secondary-foreground" className="size-4 cursor-pointer text-secondary-foreground"
onClick={handleSetSearchHistory} onClick={handleSetSearchHistory}
/> />
@ -550,7 +569,13 @@ export default function InputWithTags({
> >
{filterType}: {filterType}:
{filterType === "before" || filterType === "after" {filterType === "before" || filterType === "after"
? new Date(filterValues as number).toLocaleDateString() ? new Date(
(filterType === "before"
? (filterValues as number) + 1
: (filterValues as number)) * 1000,
).toLocaleDateString(
window.navigator?.language || "en-US",
)
: filterValues} : filterValues}
<button <button
onClick={() => onClick={() =>
@ -573,8 +598,9 @@ export default function InputWithTags({
{!currentFilterType && {!currentFilterType &&
!inputValue && !inputValue &&
searchHistoryLoaded &&
(searchHistory?.length ?? 0) > 0 && ( (searchHistory?.length ?? 0) > 0 && (
<CommandGroup heading="Previous Searches"> <CommandGroup heading="Saved Searches">
{searchHistory?.map((suggestion, index) => ( {searchHistory?.map((suggestion, index) => (
<CommandItem <CommandItem
key={index} key={index}

View File

@ -10,6 +10,7 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useState } from "react"; import { useState } from "react";
import { isMobile } from "react-device-detect";
type SaveSearchDialogProps = { type SaveSearchDialogProps = {
isOpen: boolean; isOpen: boolean;
@ -34,7 +35,13 @@ export function SaveSearchDialog({
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent> <DialogContent
onOpenAutoFocus={(e) => {
if (isMobile) {
e.preventDefault();
}
}}
>
<DialogHeader> <DialogHeader>
<DialogTitle>Save Search</DialogTitle> <DialogTitle>Save Search</DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
@ -43,14 +50,19 @@ export function SaveSearchDialog({
</DialogHeader> </DialogHeader>
<Input <Input
value={searchName} value={searchName}
className="text-md"
onChange={(e) => setSearchName(e.target.value)} onChange={(e) => setSearchName(e.target.value)}
placeholder="Enter a name for your search" placeholder="Enter a name for your search"
/> />
<DialogFooter> <DialogFooter>
<Button onClick={onClose} variant="select"> <Button onClick={onClose}>Cancel</Button>
Cancel <Button
onClick={handleSave}
variant="select"
className="mb-2 md:mb-0"
>
Save
</Button> </Button>
<Button onClick={handleSave}>Save</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -296,3 +296,63 @@ export function isCurrentHour(timestamp: number) {
return timestamp > now.getTime() / 1000; return timestamp > now.getTime() / 1000;
} }
export const convertLocalDateToTimestamp = (dateString: string): number => {
// Ensure the date string is in the correct format (8 digits)
if (!/^\d{8}$/.test(dateString)) {
return 0;
}
// Determine the local date format
const format = new Intl.DateTimeFormat()
.formatToParts(new Date())
.reduce((acc, part) => {
if (part.type === "day") acc.push("D");
if (part.type === "month") acc.push("M");
if (part.type === "year") acc.push("Y");
return acc;
}, [] as string[])
.join("");
let day: string, month: string, year: string;
// Parse the date string according to the detected format
switch (format) {
case "DMY":
[day, month, year] = [
dateString.slice(0, 2),
dateString.slice(2, 4),
dateString.slice(4),
];
break;
case "MDY":
[month, day, year] = [
dateString.slice(0, 2),
dateString.slice(2, 4),
dateString.slice(4),
];
break;
case "YMD":
[year, month, day] = [
dateString.slice(0, 2),
dateString.slice(2, 4),
dateString.slice(4),
];
break;
default:
return 0;
}
// Create a Date object based on the local timezone
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
// Check if the date is valid
if (isNaN(localDate.getTime())) {
return 0;
}
// Convert local date to UTC timestamp
const timestamp = localDate.getTime();
return timestamp;
};