frigate/web/src/components/icons/IconPicker.tsx
GuoQing Liu d34533981f
feat: add i18n (translation/localization) (#16877)
* Translation module init

* Add more i18n keys

* fix: fix string wrong

* refactor: use namespace translation file

* chore: add more translation key

* fix: fix some page name error

* refactor: change Trans tag for t function

* chore: fix some key not work

* chore: fix SearchFilterDialog i18n key error

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: fix en i18n file filter missing some keys

* chore: add some i18n keys

* chore: add more i18n keys again

* feat: add search page i18n

* feat: add explore model i18n keys

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/menu/GeneralSettings.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more live i18n keys

* feat: add more search setting i18n keys

* fix: remove some comment

* fix: fix some setting page url error

* Update web/src/views/settings/SearchSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* fix: add system missing keys

* fix: update password update i18n keys

* chore: remove outdate translation.json file

* fix: fix exploreSettings error

* chore: add object setting i18n keys

* Update web/src/views/recording/RecordingView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/public/locales/en/components/filter.json

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/components/overlay/ExportDialog.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add more i18n keys

* fix: fix motionDetectionTuner html node

* feat: add more page i18n keys

* fix: cameraStream i18n keys error

* feat: add Player i18n keys

* feat: add more toast i18n keys

* feat: change explore setting name

* feat: add more document title i18n keys

* feat: add more search i18n keys

* fix: fix accessDenied i18n keys error

* chore: add objectType i18n

* chore: add  inputWithTags i18n

* chore: add SearchFilterDialog i18n

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update web/src/views/settings/ObjectSettingsView.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* chore: add some missing i18n keys

* chore: remove most import { t } from "i18next";

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-03-16 10:36:20 -05:00

164 lines
4.8 KiB
TypeScript

import React, { useCallback, useMemo, useRef, useState } from "react";
import { IconType } from "react-icons";
import * as LuIcons from "react-icons/lu";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { IoClose } from "react-icons/io5";
import Heading from "../ui/heading";
import { cn } from "@/lib/utils";
import { Button } from "../ui/button";
import { useTranslation } from "react-i18next";
export type IconName = keyof typeof LuIcons;
export type IconElement = {
name?: string;
Icon?: IconType;
};
type IconPickerProps = {
selectedIcon?: IconElement;
setSelectedIcon?: React.Dispatch<
React.SetStateAction<IconElement | undefined>
>;
};
export default function IconPicker({
selectedIcon,
setSelectedIcon,
}: IconPickerProps) {
const { t } = useTranslation(["components/icons"]);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState("");
const iconSets = useMemo(() => [...Object.entries(LuIcons)], []);
const icons = useMemo(
() =>
iconSets.filter(
([name]) =>
name.toLowerCase().includes(searchTerm.toLowerCase()) ||
searchTerm === "",
),
[iconSets, searchTerm],
);
const handleIconSelect = useCallback(
({ name, Icon }: IconElement) => {
if (setSelectedIcon) {
setSelectedIcon({ name, Icon });
}
setSearchTerm("");
},
[setSelectedIcon],
);
return (
<div ref={containerRef}>
<Popover
open={open}
onOpenChange={(open) => {
setOpen(open);
}}
>
<PopoverTrigger asChild>
{!selectedIcon?.name || !selectedIcon?.Icon ? (
<Button
className="mt-2 w-full text-muted-foreground"
aria-label={t("iconPicker.selectIcon")}
>
{t("iconPicker.selectIcon")}
</Button>
) : (
<div className="hover:cursor-pointer">
<div className="my-3 flex w-full flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center gap-2">
<selectedIcon.Icon size={15} />
<div className="text-sm">
{selectedIcon.name
.replace(/^Lu/, "")
.replace(/([A-Z])/g, " $1")}
</div>
</div>
<IoClose
className="mx-2 hover:cursor-pointer"
onClick={() => {
handleIconSelect({ name: undefined, Icon: undefined });
}}
/>
</div>
</div>
)}
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
container={containerRef.current}
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
>
<div className="mb-3 flex flex-row items-center justify-between">
<Heading as="h4">{t("iconPicker.selectIcon")}</Heading>
<span tabIndex={0} className="sr-only" />
<IoClose
size={15}
className="hover:cursor-pointer"
onClick={() => {
setOpen(false);
}}
/>
</div>
<Input
type="text"
placeholder={t("iconPicker.search.placeholder", {
ns: "components/icons",
})}
className="text-md mb-3 md:text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="scrollbar-container flex h-full flex-col overflow-y-auto">
<div className="grid grid-cols-6 gap-2 pr-1">
{icons.map(([name, Icon]) => (
<div
key={name}
className={cn(
"flex flex-row items-center justify-center rounded-lg p-1 hover:cursor-pointer",
selectedIcon?.name === name
? "bg-selected text-white"
: "hover:bg-secondary-foreground",
)}
>
<Icon
className="size-6"
onClick={() => {
handleIconSelect({ name, Icon });
setOpen(false);
}}
/>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}
type IconRendererProps = {
icon: IconType;
size?: number;
className?: string;
};
export function IconRenderer({ icon, size, className }: IconRendererProps) {
return <>{React.createElement(icon, { size, className })}</>;
}