feat: Implement flexible media sorting by standard columns and metadata tags by refactoring backend query building and updating frontend API parameters.
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
shareAlbum,
|
||||
updateAlbum,
|
||||
type AddMediaToAlbumPayload,
|
||||
type CreateAlbumPayload,
|
||||
type RemoveMediaFromAlbumPayload,
|
||||
type SetAlbumThumbnailPayload,
|
||||
type ShareAlbumPayload,
|
||||
@@ -150,7 +149,7 @@ export const useRemoveMediaFromAlbum = (albumId: string) => {
|
||||
* Mutation hook to share an album with another user.
|
||||
*/
|
||||
export const useShareAlbum = (albumId: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
// const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (payload: ShareAlbumPayload) => shareAlbum(albumId, payload),
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -17,10 +17,17 @@ const MEDIA_KEY = ["media"];
|
||||
* Query hook to fetch a paginated list of all media.
|
||||
* This uses `useInfiniteQuery` for "load more" functionality.
|
||||
*/
|
||||
export const useGetMediaList = () => {
|
||||
export const useGetMediaList = (
|
||||
params: {
|
||||
sort_by?: string;
|
||||
order?: "asc" | "desc";
|
||||
mime_type?: string;
|
||||
} = {}
|
||||
) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [MEDIA_KEY, "list"],
|
||||
queryFn: ({ pageParam = 1 }) => getMediaList({ page: pageParam, limit: 20 }),
|
||||
queryKey: [MEDIA_KEY, "list", params],
|
||||
queryFn: ({ pageParam = 1 }) =>
|
||||
getMediaList({ page: pageParam, limit: 20, ...params }),
|
||||
getNextPageParam: (lastPage) => {
|
||||
return lastPage.has_next_page ? lastPage.page + 1 : undefined;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
@@ -3,16 +3,26 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AuthenticatedImage } from "@/components/media/authenticated-image";
|
||||
import type { Media } from "@/domain/types";
|
||||
import { useMemo, useState } from "react"; // Import useMemo
|
||||
import { useMemo, useState } from "react";
|
||||
import { MediaViewer } from "@/components/media/media-viewer";
|
||||
import { groupMediaByDate } from "@/lib/date-utils"; // Import our new helper
|
||||
import { parseISO } from "date-fns";
|
||||
import { groupMediaByDate } from "@/lib/date-utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export const Route = createFileRoute("/media/")({
|
||||
component: MediaPage,
|
||||
});
|
||||
|
||||
function MediaPage() {
|
||||
const [sortBy, setSortBy] = useState<string>("created_at");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [mimeType, setMimeType] = useState<string | undefined>(undefined);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
@@ -20,20 +30,16 @@ function MediaPage() {
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useGetMediaList();
|
||||
} = useGetMediaList({
|
||||
sort_by: sortBy,
|
||||
order: sortOrder,
|
||||
mime_type: mimeType === "all" ? undefined : mimeType,
|
||||
});
|
||||
|
||||
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
|
||||
|
||||
const allMedia = useMemo(
|
||||
() =>
|
||||
data?.pages
|
||||
.flatMap((page) => page.data)
|
||||
.sort((a, b) => {
|
||||
// Sort by date (newest first)
|
||||
const dateA = a.date_taken ?? a.created_at;
|
||||
const dateB = b.date_taken ?? b.created_at;
|
||||
return parseISO(dateB).getTime() - parseISO(dateA).getTime();
|
||||
}) ?? [],
|
||||
() => data?.pages.flatMap((page) => page.data) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
@@ -46,8 +52,65 @@ function MediaPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<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}>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && <p>Loading photos...</p>}
|
||||
|
||||
@@ -2,11 +2,14 @@ import type { Media, MediaDetails, PaginatedResponse } from "@/domain/types"
|
||||
import apiClient from "@/services/api-client"
|
||||
|
||||
type MediaListParams = {
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
page: number;
|
||||
limit: number;
|
||||
sort_by?: string;
|
||||
order?: "asc" | "desc";
|
||||
mime_type?: string;
|
||||
};
|
||||
|
||||
const API_PREFIX = import.meta.env.VITE_PREFIX_PATH || '';
|
||||
const API_PREFIX = import.meta.env.VITE_PREFIX_PATH || "";
|
||||
|
||||
export const processMediaUrls = (media: Media): Media => ({
|
||||
...media,
|
||||
@@ -22,9 +25,12 @@ export const processMediaUrls = (media: Media): Media => ({
|
||||
export const getMediaList = async ({
|
||||
page,
|
||||
limit,
|
||||
sort_by,
|
||||
order,
|
||||
mime_type,
|
||||
}: MediaListParams): Promise<PaginatedResponse<Media>> => {
|
||||
const { data } = await apiClient.get("/media", {
|
||||
params: { page, limit },
|
||||
params: { page, limit, sort_by, order, mime_type },
|
||||
});
|
||||
|
||||
data.data = data.data.map(processMediaUrls);
|
||||
@@ -66,7 +72,7 @@ export const getMediaDetails = async (
|
||||
console.log('Data for media details: ', data);
|
||||
|
||||
// Process the media URLs in the details response
|
||||
data.file_url = `${API_PREFIX}${data.file_url}`;
|
||||
data.file_url = `${API_PREFIX}${data.file_url}`;
|
||||
data.thumbnail_url = data.thumbnail_url
|
||||
? `${API_PREFIX}${data.thumbnail_url}`
|
||||
: null;
|
||||
|
||||
Reference in New Issue
Block a user