feat: Implement advanced filtering with new filter conditions and a strategy-based query builder.

This commit is contained in:
2025-12-03 23:39:47 +01:00
parent 15177f218b
commit 333c180b17
9 changed files with 481 additions and 74 deletions

View File

@@ -22,6 +22,13 @@ function MediaPage() {
const [sortBy, setSortBy] = useState<string>("created_at");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [mimeType, setMimeType] = useState<string | undefined>(undefined);
const [filters, setFilters] = useState<string[]>([]);
// Filter input state
const [filterField, setFilterField] = useState('original_filename');
const [customFieldName, setCustomFieldName] = useState('');
const [filterOperator, setFilterOperator] = useState('like');
const [filterValue, setFilterValue] = useState('');
const {
data,
@@ -30,11 +37,7 @@ function MediaPage() {
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetMediaList({
sort_by: sortBy,
order: sortOrder,
mime_type: mimeType === "all" ? undefined : mimeType,
});
} = useGetMediaList(1, 20, sortBy, sortOrder, mimeType, filters);
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
@@ -50,67 +53,145 @@ function MediaPage() {
[groupedMedia]
);
const handleAddFilter = () => {
const field = filterField === 'custom' ? customFieldName : filterField;
if (field && filterValue) {
const newFilter = `${field}:${filterOperator}:${filterValue}`;
setFilters([...filters, newFilter]);
setFilterValue(''); // Clear value after adding
}
};
const handleRemoveFilter = (index: number) => {
const newFilters = [...filters];
newFilters.splice(index, 1);
setFilters(newFilters);
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<h1 className="text-3xl font-bold">All Photos</h1>
<div className="flex flex-wrap items-center gap-2">
<Select value={sortBy} onValueChange={setSortBy}>
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<h1 className="text-3xl font-bold">All Photos</h1>
<div className="flex flex-wrap items-center gap-2">
<Select value={mimeType || "all"} onValueChange={(val) => setMimeType(val === "all" ? undefined : val)}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="image/jpeg">JPEG</SelectItem>
<SelectItem value="image/png">PNG</SelectItem>
<SelectItem value="video/mp4">MP4</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at">Date Created</SelectItem>
<SelectItem value="date_taken">Date Taken</SelectItem>
<SelectItem value="original_filename">Filename</SelectItem>
<SelectItem value="custom">Custom Tag...</SelectItem>
</SelectContent>
</Select>
{sortBy === "custom" && (
<input
type="text"
placeholder="Enter tag name"
className="border rounded px-2 py-1 text-sm w-[140px]"
onBlur={(e) => {
if (e.target.value) setSortBy(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setSortBy(e.currentTarget.value);
}
}}
/>
)}
<Select
value={sortOrder}
onValueChange={(val) => setSortOrder(val as "asc" | "desc")}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Order" />
</SelectTrigger>
<SelectContent>
<SelectItem value="desc">Newest First</SelectItem>
<SelectItem value="asc">Oldest First</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Advanced Filters */}
<div className="flex flex-wrap gap-2 items-center border p-2 rounded bg-gray-50">
<span className="text-sm font-semibold">Add Filter:</span>
<Select value={filterField} onValueChange={setFilterField}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Sort by" />
<SelectValue placeholder="Field" />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at">Date Created</SelectItem>
<SelectItem value="date_taken">Date Taken</SelectItem>
<SelectItem value="original_filename">Filename</SelectItem>
<SelectItem value="custom">Custom Tag...</SelectItem>
<SelectItem value="metadata.Make">Make</SelectItem>
<SelectItem value="metadata.Model">Model</SelectItem>
<SelectItem value="metadata.ISO">ISO</SelectItem>
<SelectItem value="metadata.FNumber">F-Number</SelectItem>
<SelectItem value="custom">Custom Field...</SelectItem>
</SelectContent>
</Select>
{sortBy === "custom" && (
{filterField === 'custom' && (
<input
type="text"
placeholder="Enter tag name"
className="border rounded px-2 py-1 text-sm w-[140px]"
onBlur={(e) => {
if (e.target.value) setSortBy(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setSortBy(e.currentTarget.value);
}
}}
className="border rounded px-2 py-1 text-sm w-[120px]"
placeholder="Field name"
value={customFieldName}
onChange={(e) => setCustomFieldName(e.target.value)}
/>
)}
<Select
value={sortOrder}
onValueChange={(val) => setSortOrder(val as "asc" | "desc")}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Order" />
<Select value={filterOperator} onValueChange={setFilterOperator}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="Op" />
</SelectTrigger>
<SelectContent>
<SelectItem value="desc">Newest First</SelectItem>
<SelectItem value="asc">Oldest First</SelectItem>
<SelectItem value="eq">=</SelectItem>
<SelectItem value="neq">!=</SelectItem>
<SelectItem value="like">Like</SelectItem>
<SelectItem value="gt">&gt;</SelectItem>
<SelectItem value="lt">&lt;</SelectItem>
<SelectItem value="gte">&gt;=</SelectItem>
<SelectItem value="lte">&lt;=</SelectItem>
</SelectContent>
</Select>
<Select
value={mimeType ?? "all"}
onValueChange={(val) => setMimeType(val === "all" ? undefined : val)}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Filter by Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="image/jpeg">JPEG</SelectItem>
<SelectItem value="image/png">PNG</SelectItem>
<SelectItem value="video/mp4">MP4</SelectItem>
</SelectContent>
</Select>
<input
type="text"
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
placeholder="Value"
className="border rounded px-2 py-1 text-sm w-[150px]"
onKeyDown={(e) => e.key === 'Enter' && handleAddFilter()}
/>
<Button onClick={handleAddFilter} size="sm">Add</Button>
</div>
{/* Active Filters List */}
{filters.length > 0 && (
<div className="flex gap-2 flex-wrap">
{filters.map((f, i) => (
<div key={i} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm flex items-center gap-2 border border-blue-200">
<span>{f.replace(/:/g, ' ')}</span>
<button onClick={() => handleRemoveFilter(i)} className="text-red-500 font-bold hover:text-red-700">×</button>
</div>
))}
</div>
)}
</div>
{isLoading && <p>Loading photos...</p>}