diff --git a/bun.lockb b/bun.lockb index cf736c2..9045f2d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5be8d3b..e2ee7ae 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", @@ -18,6 +19,7 @@ "axios": "^1.7.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "1.0.0", "date-fns": "^3.6.0", "lucide-react": "^0.428.0", "next": "14.2.5", diff --git a/src/components/people/PeopleList.tsx b/src/components/people/PeopleList.tsx index 372708d..6a3c48e 100644 --- a/src/components/people/PeopleList.tsx +++ b/src/components/people/PeopleList.tsx @@ -7,6 +7,8 @@ import PersonItem from "./PersonItem"; import { ColumnDef } from "@tanstack/react-table"; import { Table } from "../ui/table"; import PersonBirthdayCell from "./PersonBirthdayCell"; +import Link from "next/link"; +import { ENV } from "@/config/environment"; const columns: ColumnDef[] = [ { @@ -19,6 +21,22 @@ const columns: ColumnDef[] = [ accessorKey: "birthDate", cell: ({ row }) => , }, + { + header: "Hidden", + accessorKey: "isHidden", + cell: ({ row }) => ( + {row.original.isHidden ? "Yes" : "No"} + ), + }, + { + header: "Link", + accessorKey: "id", + cell: ({ row }) => ( + + View + + ) + }, { header: "Updated At", accessorKey: "updatedAt", diff --git a/src/components/people/PersonItem.tsx b/src/components/people/PersonItem.tsx index 5e9d714..29ddc86 100644 --- a/src/components/people/PersonItem.tsx +++ b/src/components/people/PersonItem.tsx @@ -2,6 +2,7 @@ import { IPerson } from "@/types/person"; import React, { useState } from "react"; import { Avatar } from "../ui/avatar"; import { updatePerson } from "@/handlers/api/people.handler"; +import { PersonMergeDropdown } from "./PersonMergeDropdown"; interface IProps { person: IPerson; @@ -54,6 +55,7 @@ export default function PersonItem({ person }: IProps) { onBlur={handleEdit} /> )} + ); } diff --git a/src/components/people/PersonMergeDropdown.tsx b/src/components/people/PersonMergeDropdown.tsx new file mode 100644 index 0000000..dfec675 --- /dev/null +++ b/src/components/people/PersonMergeDropdown.tsx @@ -0,0 +1,136 @@ +"use client"; + +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Merge } from "lucide-react"; +import { mergePerson, searchPeople } from "@/handlers/api/people.handler"; +import { IPerson } from "@/types/person"; +import { set } from "date-fns"; +import { CommandLoading } from "cmdk"; +import { Avatar } from "../ui/avatar"; + +type Status = { + value: string; + label: string; +}; + +const statuses: Status[] = [ + { + value: "backlog", + label: "Backlog", + }, + { + value: "todo", + label: "Todo", + }, + { + value: "in progress", + label: "In Progress", + }, + { + value: "done", + label: "Done", + }, + { + value: "canceled", + label: "Canceled", + }, +]; + +interface PersonMergeDropdownProps { + person: IPerson; +} +export function PersonMergeDropdown( + { person }: PersonMergeDropdownProps +) { + const [searchedPeople, setSearchedPeople] = React.useState([]); + const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const searchTimer = React.useRef(); + + const handleSearch = (value: string) => { + if (searchTimer.current) { + clearTimeout(searchTimer.current); + } + + searchTimer.current = setTimeout(() => { + setLoading(true); + if (!value) { + setSearchedPeople([]); + setLoading(false); + return; + } + + return searchPeople(value) + .then(setSearchedPeople) + .finally(() => { + setLoading(false); + }); + }, 500); + }; + + const handleSelect = (value: IPerson) => { + return mergePerson(person.id, value.id) + .then(() => { + setOpen(false); + }) + }; + + console.log(searchedPeople); + return ( +
+ + + + + + + + + {loading && } + + {searchedPeople.map((person) => ( + { + handleSelect(person); + }} + className="flex items-center gap-2" + > + + {person.name} + + ))} + + + + + +
+ ); +} diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 6c36f3f..1e75f33 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -1,10 +1,8 @@ import * as React from "react" -import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" import { DayPicker } from "react-day-picker" import "react-day-picker/dist/style.css"; import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" export type CalendarProps = React.ComponentProps diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..a0225c4 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..5d16351 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/config/routes.ts b/src/config/routes.ts index e6d18c0..d611ac5 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,5 +1,7 @@ export const BASE_ENDPOINT = '/api/immich-proxy'; export const LIST_PEOPLE_PATH = BASE_ENDPOINT + "/people"; +export const SEARCH_PEOPLE_PATH = BASE_ENDPOINT + "/search/person"; export const PERSON_THUBNAIL_PATH = (id: string) => BASE_ENDPOINT + "/thumbnail/" + id; -export const UPDATE_PERSON_PATH = (id: string) => BASE_ENDPOINT + "/people/" + id; \ No newline at end of file +export const UPDATE_PERSON_PATH = (id: string) => BASE_ENDPOINT + "/people/" + id; +export const MERGE_PERSON_PATH = (id: string) => BASE_ENDPOINT + "/people/" + id + "/merge"; \ No newline at end of file diff --git a/src/handlers/api/people.handler.ts b/src/handlers/api/people.handler.ts index 77744d6..1263292 100644 --- a/src/handlers/api/people.handler.ts +++ b/src/handlers/api/people.handler.ts @@ -1,4 +1,4 @@ -import { LIST_PEOPLE_PATH, UPDATE_PERSON_PATH } from "@/config/routes" +import { LIST_PEOPLE_PATH, MERGE_PERSON_PATH, SEARCH_PEOPLE_PATH, UPDATE_PERSON_PATH } from "@/config/routes" import { cleanUpPerson } from "@/helpers/person.helper"; import API from "@/lib/api" import { IPeopleListResponse, IPerson } from "@/types/person" @@ -18,4 +18,12 @@ export const updatePerson = (id: string, data: Partial<{ isHidden: boolean; }>) => { return API.put(UPDATE_PERSON_PATH(id), data) +} + +export const searchPeople = (name: string) => { + return API.get(SEARCH_PEOPLE_PATH, { name }).then((response) => response.map(cleanUpPerson)); +} + +export const mergePerson = (id: string, targetId: string) => { + return API.post(MERGE_PERSON_PATH(id), { ids: [ targetId ] }) } \ No newline at end of file