diff --git a/src/app/exicon/ExiconClientPageContent.tsx b/src/app/exicon/ExiconClientPageContent.tsx index 5117e9e..a666b1c 100644 --- a/src/app/exicon/ExiconClientPageContent.tsx +++ b/src/app/exicon/ExiconClientPageContent.tsx @@ -9,6 +9,7 @@ import { EntryGrid } from "@/components/shared/EntryGrid"; import type { ExiconEntry, Tag, FilterLogic, AnyEntry } from "@/lib/types"; import { exportToCSV } from "@/lib/utils"; import { TagFilter } from "@/components/exicon/TagFilter"; +import { DateRangeFilter } from "@/components/shared/DateRangeFilter"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Filter, Star } from "lucide-react"; import Link from "next/link"; @@ -51,6 +52,8 @@ export const ExiconClientPageContent = ({ const [filterLetter, setFilterLetter] = useState("All"); const [selectedTags, setSelectedTags] = useState([]); const [filterLogic, setFilterLogic] = useState("OR"); + const [dateFrom, setDateFrom] = useState(""); + const [dateTo, setDateTo] = useState(""); const [isInitialized, setIsInitialized] = useState(false); // Parse URL parameters on mount @@ -59,6 +62,8 @@ export const ExiconClientPageContent = ({ const tagLogicParam = searchParams.get("tagLogic"); const searchParam = searchParams.get("search"); const letterParam = searchParams.get("letter"); + const dateFromParam = searchParams.get("dateFrom"); + const dateToParam = searchParams.get("dateTo"); const tagNames = tagsParam ? tagsParam.split(",").map((t) => t.trim()) : []; const nextSelectedTags = tagNames @@ -77,6 +82,8 @@ export const ExiconClientPageContent = ({ letterParam && letterParam.length === 1 ? letterParam.toUpperCase() : "All"; + const nextDateFrom = dateFromParam ?? ""; + const nextDateTo = dateToParam ?? ""; startTransition(() => { setSelectedTags((prev) => @@ -91,6 +98,8 @@ export const ExiconClientPageContent = ({ setFilterLetter((prev) => prev === nextFilterLetter ? prev : nextFilterLetter, ); + setDateFrom((prev) => (prev === nextDateFrom ? prev : nextDateFrom)); + setDateTo((prev) => (prev === nextDateTo ? prev : nextDateTo)); setIsInitialized((prev) => (prev ? prev : true)); }); }, [searchParams, allTags]); @@ -122,6 +131,14 @@ export const ExiconClientPageContent = ({ params.set("letter", filterLetter); } + if (dateFrom) { + params.set("dateFrom", dateFrom); + } + + if (dateTo) { + params.set("dateTo", dateTo); + } + const nextSearch = params.toString(); if (nextSearch === searchParamsString) { @@ -135,6 +152,8 @@ export const ExiconClientPageContent = ({ filterLogic, searchTerm, filterLetter, + dateFrom, + dateTo, isInitialized, allTags, router, @@ -251,10 +270,20 @@ export const ExiconClientPageContent = ({ ); }; + const matchesDate = () => { + if (!dateFrom && !dateTo) return true; + if (!entry.createdAt) return true; + const entryDate = entry.createdAt.substring(0, 10); + if (dateFrom && entryDate < dateFrom) return false; + if (dateTo && entryDate > dateTo) return false; + return true; + }; + return { entry, searchPriority, - matches: matchesSearch && matchesLetter && matchesTags(), + matches: + matchesSearch && matchesLetter && matchesTags() && matchesDate(), }; }) .filter((item) => item.matches) @@ -267,7 +296,7 @@ export const ExiconClientPageContent = ({ return a.entry.name.localeCompare(b.entry.name); }) .map((item) => item.entry); - }, [initialEntries, searchTerm, filterLetter, selectedTags, filterLogic]); + }, [initialEntries, searchTerm, filterLetter, selectedTags, filterLogic, dateFrom, dateTo]); return (
@@ -309,6 +338,13 @@ export const ExiconClientPageContent = ({ filterLogic={filterLogic} onFilterLogicChange={setFilterLogic} /> + + {/* Main Content */} diff --git a/src/app/lexicon/LexiconClientPageContent.tsx b/src/app/lexicon/LexiconClientPageContent.tsx index 71e80e3..203e7aa 100644 --- a/src/app/lexicon/LexiconClientPageContent.tsx +++ b/src/app/lexicon/LexiconClientPageContent.tsx @@ -7,6 +7,7 @@ import { SearchBar } from "@/components/shared/SearchBar"; import { Button } from "@/components/ui/button"; import { Download, BookText, PencilLine } from "lucide-react"; import { EntryGrid } from "@/components/shared/EntryGrid"; +import { DateRangeFilter } from "@/components/shared/DateRangeFilter"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Filter, Star } from "lucide-react"; import Link from "next/link"; @@ -79,18 +80,24 @@ export const LexiconClientPageContent = ({ const [searchTerm, setSearchTerm] = useState(""); const [filterLetter, setFilterLetter] = useState("All"); + const [dateFrom, setDateFrom] = useState(""); + const [dateTo, setDateTo] = useState(""); const [isInitialized, setIsInitialized] = useState(false); // Parse URL parameters on mount useEffect(() => { const searchParam = searchParams.get("search"); const letterParam = searchParams.get("letter"); + const dateFromParam = searchParams.get("dateFrom"); + const dateToParam = searchParams.get("dateTo"); const nextSearchTerm = searchParam ?? ""; const nextFilterLetter = letterParam && letterParam.length === 1 ? letterParam.toUpperCase() : "All"; + const nextDateFrom = dateFromParam ?? ""; + const nextDateTo = dateToParam ?? ""; startTransition(() => { setSearchTerm((prev) => @@ -99,6 +106,8 @@ export const LexiconClientPageContent = ({ setFilterLetter((prev) => prev === nextFilterLetter ? prev : nextFilterLetter, ); + setDateFrom((prev) => (prev === nextDateFrom ? prev : nextDateFrom)); + setDateTo((prev) => (prev === nextDateTo ? prev : nextDateTo)); setIsInitialized((prev) => (prev ? prev : true)); }); }, [searchParams]); @@ -117,9 +126,17 @@ export const LexiconClientPageContent = ({ params.set("letter", filterLetter); } + if (dateFrom) { + params.set("dateFrom", dateFrom); + } + + if (dateTo) { + params.set("dateTo", dateTo); + } + const newUrl = params.toString() ? `?${params.toString()}` : "/lexicon"; router.replace(newUrl, { scroll: false }); - }, [searchTerm, filterLetter, isInitialized, router]); + }, [searchTerm, filterLetter, dateFrom, dateTo, isInitialized, router]); // Function to handle the letter filter button clicks const handleFilterLetterChange = (letter: string) => { @@ -221,10 +238,19 @@ export const LexiconClientPageContent = ({ filterLetter === "All" || entry.name.toLowerCase().startsWith(filterLetter.toLowerCase()); + const matchesDate = () => { + if (!dateFrom && !dateTo) return true; + if (!entry.createdAt) return true; + const entryDate = entry.createdAt.substring(0, 10); + if (dateFrom && entryDate < dateFrom) return false; + if (dateTo && entryDate > dateTo) return false; + return true; + }; + return { entry, searchPriority, - matches: matchesLetter && matchesSearch, + matches: matchesLetter && matchesSearch && matchesDate(), }; }) .filter((item) => item.matches) @@ -237,7 +263,7 @@ export const LexiconClientPageContent = ({ return a.entry.name.localeCompare(b.entry.name); }) .map((item) => item.entry); - }, [initialEntries, searchTerm, filterLetter]); + }, [initialEntries, searchTerm, filterLetter, dateFrom, dateTo]); return (
@@ -271,6 +297,13 @@ export const LexiconClientPageContent = ({
+ + {/* Main Content */} diff --git a/src/components/shared/DateRangeFilter.tsx b/src/components/shared/DateRangeFilter.tsx new file mode 100644 index 0000000..f86df44 --- /dev/null +++ b/src/components/shared/DateRangeFilter.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { format, subDays, subYears } from "date-fns"; +import { CalendarIcon, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +interface DateRangeFilterProps { + dateFrom: string; + dateTo: string; + onDateFromChange: (value: string) => void; + onDateToChange: (value: string) => void; +} + +export function DateRangeFilter({ + dateFrom, + dateTo, + onDateFromChange, + onDateToChange, +}: DateRangeFilterProps) { + const today = new Date(); + + const applyPreset = (from: Date, to?: Date) => { + onDateFromChange(format(from, "yyyy-MM-dd")); + onDateToChange(to ? format(to, "yyyy-MM-dd") : ""); + }; + + const clearFilter = () => { + onDateFromChange(""); + onDateToChange(""); + }; + + const isActive = dateFrom !== "" || dateTo !== ""; + + const parsedFrom = dateFrom ? new Date(dateFrom + "T00:00:00") : undefined; + const parsedTo = dateTo ? new Date(dateTo + "T00:00:00") : undefined; + + return ( + + + + + + Filter by Date Added + + {isActive && ( + + )} + + + + {/* Quick Presets */} +
+ + + + +
+ + {/* Custom Date Pickers */} +
+
+ + + + + + + + onDateFromChange(day ? format(day, "yyyy-MM-dd") : "") + } + initialFocus + /> + + +
+
+ + + + + + + + onDateToChange(day ? format(day, "yyyy-MM-dd") : "") + } + initialFocus + /> + + +
+
+
+
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index ff5e672..2d3c94d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -90,6 +90,9 @@ export const transformDbRowToEntry = (row: any): EntryWithReferences => { tags: row.tags || [], videoLink: row.video_link, mentionedEntries: mentionedEntries, + createdAt: row.created_at + ? new Date(row.created_at).toISOString() + : undefined, referencedBy: row.referenced_by_data ? row.referenced_by_data.map((ref: any) => ref.id) : [], @@ -357,6 +360,7 @@ export const fetchAllEntries = async ( e.aliases, e.video_link, e.mentioned_entries, + e.created_at, COALESCE( ( SELECT json_agg( diff --git a/src/lib/types.ts b/src/lib/types.ts index 054a0aa..648b9de 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -28,6 +28,7 @@ export interface BaseEntry { references?: ReferencedEntry[]; mentionedEntries?: string[]; resolvedMentionsData?: Record; + createdAt?: string; } export interface ExiconEntry extends BaseEntry { type: "exicon";