Skip to content

Commit

Permalink
Merge pull request #420 from aXenDeveloper/tag_input
Browse files Browse the repository at this point in the history
feat(frontend): Add tag input
  • Loading branch information
aXenDeveloper authored Jul 20, 2024
2 parents a50a6ae + 404b6ef commit 1e29da5
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 27 deletions.
74 changes: 74 additions & 0 deletions apps/docs/content/docs/ui/tag-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: Tag Input
description: Tag Input is a component that allows users to input tags.
---

## Preview

Image

## Props

| Name | Type | Default | Description |
| ----------- | --------- | ------- | -------------------- |
| placeholder | `string` | | Placeholder text. |
| multiple | `boolean` | false | Allow multiple tags. |
| disabled | `boolean` | false | Disable input. |

## Usage

```tsx
const formSchema = z.object({
tag_input_test: z.object({ id: z.number(), value: z.string() }),
});
```

```tsx
import { TagInput } from 'vitnode-frontend/components/ui/tag-input';
```

```tsx
<FormField
control={form.control}
name="tag_input_test"
render={({ field }) => (
<FormItem>
<FormLabel>Tag Input Test</FormLabel>
<FormControl>
<TagInput {...field} /> // [!code highlight]
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```

### Multiple

```tsx
const formSchema = z.object({
tag_input_test: z.array(z.object({ id: z.number(), value: z.string() })),
});
```

```tsx
import { TagInput } from 'vitnode-frontend/components/ui/tag-input';
```

```tsx
<FormField
control={form.control}
name="tag_input_test"
render={({ field }) => (
<FormItem>
<FormLabel>Tag Input Test</FormLabel>
<FormControl>
<TagInput {...field} multiple /> // [!code highlight]
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```

import { placeholder } from 'drizzle-orm';
17 changes: 0 additions & 17 deletions apps/docs/content/docs/ui/test.mdx

This file was deleted.

2 changes: 1 addition & 1 deletion packages/frontend/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cva, VariantProps } from 'class-variance-authority';
import { cn } from '../../helpers/classnames';

const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-md border px-3 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 flex items-center gap-2',
{
variants: {
variant: {
Expand Down
15 changes: 6 additions & 9 deletions packages/frontend/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ export interface InputProps
ref?: React.Ref<HTMLInputElement>;
}

const classNameInput = cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
);

const Input = ({ className, type, ...props }: InputProps) => {
return (
<input
type={type}
className={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
<input type={type} className={cn(classNameInput, className)} {...props} />
);
};

export { Input };
export { Input, classNameInput };
119 changes: 119 additions & 0 deletions packages/frontend/src/components/ui/tag-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use client';

import React from 'react';
import { X } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';

import { badgeVariants } from './badge';
import { Input } from './input';

interface TagInputItemProps {
id: number | string;
value: string;
}

interface Props
extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> {
onChange: (value?: TagInputItemProps | TagInputItemProps[]) => void;
className?: string;
disabled?: boolean;
}

interface MultiProps extends Props {
multiple?: true;
value?: TagInputItemProps[];
}

interface SingleProps extends Props {
multiple?: never;
value?: TagInputItemProps;
}

export const TagInput = ({
multiple,
onChange,
value: valueFromProps,
disabled,
...rest
}: MultiProps | SingleProps) => {
const values: TagInputItemProps[] = Array.isArray(valueFromProps)
? valueFromProps
: valueFromProps
? [valueFromProps]
: [];
const [textInput, setTextInput] = React.useState('');

return (
<div className="space-y-3">
{values.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
<AnimatePresence>
{values.map(item => {
const onRemove = () => {
if (multiple) {
onChange(values.filter(value => value.id !== item.id));

return;
}

onChange();
};

return (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
layout
className={badgeVariants({
variant: 'outline',
className: 'shrink-0 cursor-pointer [&>svg]:size-4',
})}
key={item.id}
tabIndex={0}
onClick={e => {
e.stopPropagation();
e.preventDefault();
onRemove();
}}
onKeyDown={e => {
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
onRemove();
}
}}
>
{item.value} <X />
</motion.div>
);
})}
</AnimatePresence>
</div>
)}

<Input
onChange={e => setTextInput(e.target.value)}
value={textInput}
disabled={(!multiple && values.length > 0) || disabled}
onKeyDown={e => {
if ((e.key === 'Enter' || e.key === ',') && textInput) {
e.preventDefault();
const items = textInput.split(',').map(value => value.trim());

onChange([
...values,
...items.map(value => ({
id: Math.random(),
value,
})),
]);
setTextInput('');
}
}}
{...rest}
type="text"
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useTranslations } from 'next-intl';
import { AlertCircle } from 'lucide-react';
import React from 'react';

import {
Form,
Expand Down

0 comments on commit 1e29da5

Please sign in to comment.