Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/app/exicon/ExiconClientPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,6 +52,8 @@ export const ExiconClientPageContent = ({
const [filterLetter, setFilterLetter] = useState("All");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [filterLogic, setFilterLogic] = useState<FilterLogic>("OR");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const [isInitialized, setIsInitialized] = useState(false);

// Parse URL parameters on mount
Expand All @@ -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
Expand All @@ -77,6 +82,8 @@ export const ExiconClientPageContent = ({
letterParam && letterParam.length === 1
? letterParam.toUpperCase()
: "All";
const nextDateFrom = dateFromParam ?? "";
const nextDateTo = dateToParam ?? "";

startTransition(() => {
setSelectedTags((prev) =>
Expand All @@ -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]);
Expand Down Expand Up @@ -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) {
Expand All @@ -135,6 +152,8 @@ export const ExiconClientPageContent = ({
filterLogic,
searchTerm,
filterLetter,
dateFrom,
dateTo,
isInitialized,
allTags,
router,
Expand Down Expand Up @@ -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)
Expand All @@ -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 (
<div className="flex flex-col md:flex-row gap-8">
Expand Down Expand Up @@ -309,6 +338,13 @@ export const ExiconClientPageContent = ({
filterLogic={filterLogic}
onFilterLogicChange={setFilterLogic}
/>

<DateRangeFilter
dateFrom={dateFrom}
dateTo={dateTo}
onDateFromChange={setDateFrom}
onDateToChange={setDateTo}
/>
</aside>

{/* Main Content */}
Expand Down
39 changes: 36 additions & 3 deletions src/app/lexicon/LexiconClientPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) =>
Expand All @@ -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]);
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)
Expand All @@ -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 (
<div className="flex flex-col md:flex-row gap-8">
Expand Down Expand Up @@ -271,6 +297,13 @@ export const LexiconClientPageContent = ({
</div>
</CardContent>
</Card>

<DateRangeFilter
dateFrom={dateFrom}
dateTo={dateTo}
onDateFromChange={setDateFrom}
onDateToChange={setDateTo}
/>
</aside>

{/* Main Content */}
Expand Down
163 changes: 163 additions & 0 deletions src/components/shared/DateRangeFilter.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card className="shadow-sm">
<CardHeader>
<CardTitle className="flex items-center justify-between text-lg">
<span className="flex items-center gap-2">
<CalendarIcon className="h-5 w-5 text-primary" />
Filter by Date Added
</span>
{isActive && (
<Button
variant="ghost"
size="sm"
onClick={clearFilter}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* Quick Presets */}
<div className="flex flex-wrap gap-1">
<button
onClick={() => applyPreset(subDays(today, 30))}
className="px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
Last 30 Days
</button>
<button
onClick={() => applyPreset(subDays(today, 90))}
className="px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
Last 90 Days
</button>
<button
onClick={() => applyPreset(subYears(today, 1))}
className="px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
Last Year
</button>
<button
onClick={() =>
applyPreset(
new Date("2000-01-01"),
new Date("2023-12-31"),
)
}
className="px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
Before 2024
</button>
</div>

{/* Custom Date Pickers */}
<div className="space-y-2">
<div>
<label className="text-xs font-medium text-muted-foreground">
From
</label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal h-8 text-xs",
!dateFrom && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-3 w-3" />
{dateFrom ? format(parsedFrom!, "MMM d, yyyy") : "Start date"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={parsedFrom}
onSelect={(day) =>
onDateFromChange(day ? format(day, "yyyy-MM-dd") : "")
}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground">
To
</label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal h-8 text-xs",
!dateTo && "text-muted-foreground",
)}
>
<CalendarIcon className="mr-2 h-3 w-3" />
{dateTo ? format(parsedTo!, "MMM d, yyyy") : "End date"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={parsedTo}
onSelect={(day) =>
onDateToChange(day ? format(day, "yyyy-MM-dd") : "")
}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
</CardContent>
</Card>
);
}
4 changes: 4 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
: [],
Expand Down Expand Up @@ -357,6 +360,7 @@ export const fetchAllEntries = async (
e.aliases,
e.video_link,
e.mentioned_entries,
e.created_at,
COALESCE(
(
SELECT json_agg(
Expand Down
Loading
Loading