Handy/src/components/settings/CustomWords.tsx
2026-02-19 17:26:14 +08:00

125 lines
3.9 KiB
TypeScript

import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useSettings } from "../../hooks/useSettings";
import { Input } from "../ui/Input";
import { Button } from "../ui/Button";
import { SettingContainer } from "../ui/SettingContainer";
interface CustomWordsProps {
descriptionMode?: "inline" | "tooltip";
grouped?: boolean;
}
export const CustomWords: React.FC<CustomWordsProps> = React.memo(
({ descriptionMode = "tooltip", grouped = false }) => {
const { t } = useTranslation();
const { getSetting, updateSetting, isUpdating } = useSettings();
const [newWord, setNewWord] = useState("");
const customWords = getSetting("custom_words") || [];
const handleAddWord = () => {
const trimmedWord = newWord.trim();
const sanitizedWord = trimmedWord.replace(/[<>"'&]/g, "");
if (
sanitizedWord &&
!sanitizedWord.includes(" ") &&
sanitizedWord.length <= 50
) {
if (customWords.includes(sanitizedWord)) {
toast.error(
t("settings.advanced.customWords.duplicate", {
word: sanitizedWord,
}),
);
return;
}
updateSetting("custom_words", [...customWords, sanitizedWord]);
setNewWord("");
}
};
const handleRemoveWord = (wordToRemove: string) => {
updateSetting(
"custom_words",
customWords.filter((word) => word !== wordToRemove),
);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddWord();
}
};
return (
<>
<SettingContainer
title={t("settings.advanced.customWords.title")}
description={t("settings.advanced.customWords.description")}
descriptionMode={descriptionMode}
grouped={grouped}
>
<div className="flex items-center gap-2">
<Input
type="text"
className="max-w-40"
value={newWord}
onChange={(e) => setNewWord(e.target.value)}
onKeyDown={handleKeyPress}
placeholder={t("settings.advanced.customWords.placeholder")}
variant="compact"
disabled={isUpdating("custom_words")}
/>
<Button
onClick={handleAddWord}
disabled={
!newWord.trim() ||
newWord.includes(" ") ||
newWord.trim().length > 50 ||
isUpdating("custom_words")
}
variant="primary"
size="md"
>
{t("settings.advanced.customWords.add")}
</Button>
</div>
</SettingContainer>
{customWords.length > 0 && (
<div
className={`px-4 p-2 ${grouped ? "" : "rounded-lg border border-mid-gray/20"} flex flex-wrap gap-1`}
>
{customWords.map((word) => (
<Button
key={word}
onClick={() => handleRemoveWord(word)}
disabled={isUpdating("custom_words")}
variant="secondary"
size="sm"
className="inline-flex items-center gap-1 cursor-pointer"
aria-label={t("settings.advanced.customWords.remove", { word })}
>
<span>{word}</span>
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</Button>
))}
</div>
)}
</>
);
},
);