feat: improve tag selector
This commit is contained in:
parent
a09b3cb56d
commit
de63677650
5 changed files with 104 additions and 73 deletions
|
|
@ -32,7 +32,7 @@ body {
|
|||
}
|
||||
|
||||
.pill {
|
||||
@apply flex justify-center items-center px-5 py-2 bg-orange-300 border-2 border-orange-400 rounded-4xl shadow-md;
|
||||
@apply flex justify-center items-center px-5 py-2 bg-orange-300 border-2 border-orange-400 rounded-3xl shadow-md;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ export default function EditForm({ mii, likes }: Props) {
|
|||
<label htmlFor="tags" className="font-semibold">
|
||||
Tags
|
||||
</label>
|
||||
<TagSelector tags={tags} setTags={setTags} />
|
||||
<TagSelector tags={tags} setTags={setTags} showTagLimit />
|
||||
</div>
|
||||
|
||||
<div className="w-full grid grid-cols-3 items-start">
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ export default function SubmitForm() {
|
|||
<label htmlFor="tags" className="font-semibold">
|
||||
Tags
|
||||
</label>
|
||||
<TagSelector tags={tags} setTags={setTags} />
|
||||
<TagSelector tags={tags} setTags={setTags} showTagLimit />
|
||||
</div>
|
||||
|
||||
<div className="w-full grid grid-cols-3 items-start">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useCombobox } from "downshift";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface Props {
|
||||
tags: string[];
|
||||
setTags: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
showTagLimit?: boolean;
|
||||
}
|
||||
|
||||
const tagRegex = /^[a-z0-9-_]*$/;
|
||||
const predefinedTags = ["anime", "art", "cartoon", "celebrity", "games", "history", "meme", "movie", "oc", "tv"];
|
||||
|
||||
export default function TagSelector({ tags, setTags }: Props) {
|
||||
export default function TagSelector({ tags, setTags, showTagLimit }: Props) {
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const getFilteredItems = (): string[] =>
|
||||
predefinedTags.filter((item) => item.toLowerCase().includes(inputValue?.toLowerCase() || "")).filter((item) => !tags.includes(item));
|
||||
|
|
@ -23,7 +25,7 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
const hasSelectedItems = tags.length > 0;
|
||||
|
||||
const addTag = (tag: string) => {
|
||||
if (!tags.includes(tag) && tags.length < 8) {
|
||||
if (!tags.includes(tag) && tags.length < 8 && tag.length <= 20) {
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
|
|
@ -32,7 +34,7 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
setTags(tags.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, getItemProps, highlightedIndex } = useCombobox<string>({
|
||||
const { isOpen, openMenu, getToggleButtonProps, getMenuProps, getInputProps, getItemProps, highlightedIndex } = useCombobox<string>({
|
||||
inputValue,
|
||||
items: filteredItems,
|
||||
onInputValueChange: ({ inputValue }) => {
|
||||
|
|
@ -61,11 +63,20 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleContainerClick = () => {
|
||||
if (!isMaxItemsSelected) {
|
||||
inputRef.current?.focus();
|
||||
openMenu();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-span-2 relative">
|
||||
<div
|
||||
className={`col-span-2 justify-between! pill input relative focus-within:ring-[3px] ring-orange-400/50 transition ${
|
||||
tags.length > 0 ? "py-1.5!" : ""
|
||||
className={`relative justify-between! pill input focus-within:ring-[3px] ring-orange-400/50 cursor-text transition ${
|
||||
tags.length > 0 ? "py-1.5! px-2!" : ""
|
||||
}`}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1.5 w-full">
|
||||
|
|
@ -89,23 +100,31 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
{/* Input */}
|
||||
<input
|
||||
{...getInputProps({
|
||||
ref: inputRef,
|
||||
onKeyDown: handleKeyDown,
|
||||
disabled: isMaxItemsSelected,
|
||||
placeholder: tags.length > 0 ? "" : "Type or select a tag...",
|
||||
maxLength: 20,
|
||||
className: "w-full flex-1 outline-none placeholder:text-black/40",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Control buttons */}
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
{hasSelectedItems && (
|
||||
<button type="button" aria-label="Remove All Tags" className="text-black cursor-pointer" onClick={() => setTags([])}>
|
||||
<Icon icon="mdi:close" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button type="button" aria-label="Toggle Tag Dropdown" {...getToggleButtonProps()} className="text-black cursor-pointer text-xl">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle Tag Dropdown"
|
||||
{...getToggleButtonProps()}
|
||||
disabled={isMaxItemsSelected}
|
||||
className="text-black cursor-pointer text-xl disabled:text-black/35"
|
||||
>
|
||||
<Icon icon="mdi:chevron-down" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -114,12 +133,12 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
{!isMaxItemsSelected && (
|
||||
<ul
|
||||
{...getMenuProps()}
|
||||
className={`absolute left-0 top-full mt-2 z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg shadow-lg max-h-60 overflow-y-auto ${
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`absolute right-0 top-full mt-2 z-50 w-80 bg-orange-200/45 backdrop-blur-md border-2 border-orange-400 rounded-lg shadow-lg shadow-black/25 max-h-60 overflow-y-auto ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{isOpen &&
|
||||
filteredItems.map((item, index) => (
|
||||
{filteredItems.map((item, index) => (
|
||||
<li
|
||||
key={item}
|
||||
{...getItemProps({ item, index })}
|
||||
|
|
@ -128,7 +147,7 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
{item}
|
||||
</li>
|
||||
))}
|
||||
{isOpen && inputValue && !filteredItems.includes(inputValue) && (
|
||||
{inputValue && !filteredItems.includes(inputValue) && (
|
||||
<li
|
||||
className="px-4 py-1 cursor-pointer text-sm bg-black/15"
|
||||
onClick={() => {
|
||||
|
|
@ -142,5 +161,17 @@ export default function TagSelector({ tags, setTags }: Props) {
|
|||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tag limit message */}
|
||||
{showTagLimit && (
|
||||
<div className="mt-1.5 text-xs min-h-4">
|
||||
{isMaxItemsSelected ? (
|
||||
<span className="text-red-400 font-medium">Maximum of 8 tags reached. Remove a tag to add more.</span>
|
||||
) : (
|
||||
<span className="text-black/60">{tags.length}/8 tags</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const tagsSchema = z
|
|||
z
|
||||
.string()
|
||||
.min(2, { error: "Tags must be at least 2 characters long" })
|
||||
.max(64, { error: "Tags cannot be more than 20 characters long" })
|
||||
.max(20, { error: "Tags cannot be more than 20 characters long" })
|
||||
.regex(/^[a-z0-9-_]+$/, {
|
||||
error: "Tags can only contain lowercase letters, numbers, dashes, and underscores.",
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue