status añadido, cambios en el responsive al ver el producto

This commit is contained in:
2025-07-14 14:13:35 -04:00
parent f4e9379c34
commit ee089f4351
18 changed files with 1754 additions and 53 deletions

View File

@@ -0,0 +1,8 @@
DROP VIEW "public"."v_product_store";--> statement-breakpoint
ALTER TABLE "products" ALTER COLUMN "price" SET DEFAULT '0';--> statement-breakpoint
ALTER TABLE "products" ALTER COLUMN "stock" SET DEFAULT 0;--> statement-breakpoint
ALTER TABLE "products" ADD COLUMN "status" text DEFAULT 'BORRADOR' NOT NULL;--> statement-breakpoint
CREATE VIEW "public"."v_product_store" AS (
select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.address, p.status, p.user_id, u.fullname, u.email, u.phone
from products p
left join auth.users as u on u.id = p.user_id);

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,13 @@
"when": 1752500607554, "when": 1752500607554,
"tag": "0005_little_bloodscream", "tag": "0005_little_bloodscream",
"breakpoints": true "breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1752507413748,
"tag": "0006_real_tyger_tiger",
"breakpoints": true
} }
] ]
} }

View File

@@ -9,10 +9,11 @@ export const products = t.pgTable(
id: t.serial('id').primaryKey(), id: t.serial('id').primaryKey(),
title: t.text('title').notNull(), title: t.text('title').notNull(),
description: t.text('description').notNull(), description: t.text('description').notNull(),
price: t.numeric('price').notNull(), price: t.numeric('price').notNull().default('0'),
stock: t.integer('stock').notNull(), stock: t.integer('stock').notNull().default(0),
address: t.text('address').notNull(), address: t.text('address').notNull(),
urlImg: t.text('url_img').notNull(), urlImg: t.text('url_img').notNull(),
status: t.text('status').notNull().default('BORRADOR'),
userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), userId: t.integer('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
...timestamps, ...timestamps,
} }
@@ -26,11 +27,12 @@ export const viewProductsStore = t.pgView('v_product_store', {
stock: t.integer('stock'), stock: t.integer('stock'),
urlImg: t.text('url_img'), urlImg: t.text('url_img'),
address: t.text('address'), address: t.text('address'),
status: t.text('status'),
userId: t.integer('user_id'), userId: t.integer('user_id'),
fullname: t.text('fullname'), fullname: t.text('fullname'),
email: t.text('email'), email: t.text('email'),
phone: t.text('phone') phone: t.text('phone')
}).as(sql` }).as(sql`
select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.address, p.user_id, u.fullname, u.email, u.phone select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.address, p.status, p.user_id, u.fullname, u.email, u.phone
from products p from products p
left join auth.users as u on u.id = p.user_id`); left join auth.users as u on u.id = p.user_id`);

View File

@@ -7,19 +7,31 @@ export async function seedProducts(db: NodePgDatabase<typeof schema>) {
console.log('Seeding example product...'); console.log('Seeding example product...');
// Insert inventory // Insert inventory
const array = [{title:'manzana',description:'fruta roja',price:'100',urlImg:'apple.avif',address:"Calle 1",userId:1,stock:0}]; const array = [
{
title:'manzana',
description:'Fruta pequeña y roja, extraída de los árboles de nuestra fundación, de increíble sabor',
price:'100',
stock:10,
address:"Calle 1",
status:'PUBLICADO', // PUBLICADO, AGOTADO, BORRADOR
urlImg:'apple.avif',
userId:1
}
];
for (const item of array) { for (const item of array) {
try { try {
await db.insert(products).values({ // await db.insert(products).values({
title: item.title, // title: item.title,
description: item.description, // description: item.description,
price: item.price, // price: item.price,
stock: item.stock, // stock: item.stock,
address: item.address, // address: item.address,
urlImg: item.urlImg, // urlImg: item.urlImg,
userId: item.userId // userId: item.userId
}).onConflictDoNothing(); // }).onConflictDoNothing();
await db.insert(products).values(item).onConflictDoNothing();
} catch (error) { } catch (error) {
console.error(`Error creating products '${item.title}':`, error); console.error(`Error creating products '${item.title}':`, error);
} }

View File

@@ -32,10 +32,17 @@ export class CreateProductDto {
address: string; address: string;
@ApiProperty() @ApiProperty()
@IsInt({ @IsString({
message: 'stock must be a number', message: 'address must be a string',
}) })
@IsOptional() @IsOptional()
status: string;
@ApiProperty()
@IsInt({
message: 'userID must be a number',
})
// @IsOptional()
userId: number; userId: number;
@ApiProperty() @ApiProperty()

View File

@@ -20,6 +20,9 @@ export class UpdateProductDto extends PartialType(CreateProductDto) {
@IsOptional() @IsOptional()
address: string; address: string;
@IsOptional()
status: string;
@IsOptional() @IsOptional()
urlImg: string; urlImg: string;
} }

View File

@@ -16,7 +16,7 @@ export class UsersController {
@ApiOperation({ summary: 'Get all products with pagination and filters' }) @ApiOperation({ summary: 'Get all products with pagination and filters' })
@ApiResponse({ status: 200, description: 'Return paginated products.' }) @ApiResponse({ status: 200, description: 'Return paginated products.' })
async findAll(@Query() paginationDto: PaginationDto) { async findAll(@Query() paginationDto: PaginationDto) {
const result = await this.inventoryService.findAll(paginationDto); const result = await this.inventoryService.findAll(paginationDto,true);
return { return {
message: 'products fetched successfully', message: 'products fetched successfully',
data: result.data, data: result.data,

View File

@@ -56,6 +56,7 @@ export class InventoryService {
address: products.address, address: products.address,
price: products.price, price: products.price,
stock: products.stock, stock: products.stock,
status: products.status,
urlImg: products.urlImg urlImg: products.urlImg
}) })
.from(products) .from(products)
@@ -78,7 +79,7 @@ export class InventoryService {
return { data, meta }; return { data, meta };
} }
async findAll(paginationDto?: PaginationDto): Promise<{ data: Store[], meta: any }> { async findAll(paginationDto?: PaginationDto, isStore: boolean = false): Promise<{ data: Store[], meta: any }> {
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
// Calculate offset // Calculate offset
@@ -86,11 +87,21 @@ export class InventoryService {
// Build search condition // Build search condition
let searchCondition: SQL<unknown> | undefined; let searchCondition: SQL<unknown> | undefined;
if (search) { if (search && isStore) {
searchCondition = or( searchCondition = and(
or(
like(viewProductsStore.title, `%${search}%`), like(viewProductsStore.title, `%${search}%`),
like(viewProductsStore.description, `%${search}%`), like(viewProductsStore.description, `%${search}%`)
); ),
or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO'))
)
} else if(search){
or(
like(viewProductsStore.title, `%${search}%`),
like(viewProductsStore.description, `%${search}%`)
)
} else if(isStore){
searchCondition = or(eq(viewProductsStore.status, 'PUBLICADO'), eq(viewProductsStore.status, 'AGOTADO'))
} }
// Build sort condition // Build sort condition
@@ -117,6 +128,7 @@ export class InventoryService {
address: viewProductsStore.address, address: viewProductsStore.address,
urlImg: viewProductsStore.urlImg, urlImg: viewProductsStore.urlImg,
stock: viewProductsStore.stock, stock: viewProductsStore.stock,
status: viewProductsStore.status,
userId: viewProductsStore.userId, userId: viewProductsStore.userId,
fullname: viewProductsStore.fullname, fullname: viewProductsStore.fullname,
email: viewProductsStore.email, email: viewProductsStore.email,
@@ -152,6 +164,7 @@ export class InventoryService {
address: viewProductsStore.address, address: viewProductsStore.address,
urlImg: viewProductsStore.urlImg, urlImg: viewProductsStore.urlImg,
stock: viewProductsStore.stock, stock: viewProductsStore.stock,
status: viewProductsStore.status,
userId: viewProductsStore.userId, userId: viewProductsStore.userId,
fullname: viewProductsStore.fullname, fullname: viewProductsStore.fullname,
email: viewProductsStore.email, email: viewProductsStore.email,
@@ -185,6 +198,7 @@ export class InventoryService {
address: createProductDto.address, address: createProductDto.address,
urlImg: createProductDto.urlImg, urlImg: createProductDto.urlImg,
stock: createProductDto.stock, stock: createProductDto.stock,
status: createProductDto.status,
userId: createProductDto.userId userId: createProductDto.userId
}) })
.returning(); .returning();
@@ -194,6 +208,7 @@ export class InventoryService {
async update(id: string, updateProductDto: UpdateProductDto): Promise<Product> { async update(id: string, updateProductDto: UpdateProductDto): Promise<Product> {
const productId = parseInt(id); const productId = parseInt(id);
console.log(updateProductDto);
// Check if exists // Check if exists
await this.findOne(id); await this.findOne(id);
@@ -204,6 +219,7 @@ export class InventoryService {
if (updateProductDto.description) updateData.description = updateProductDto.description; if (updateProductDto.description) updateData.description = updateProductDto.description;
if (updateProductDto.price) updateData.price = updateProductDto.price; if (updateProductDto.price) updateData.price = updateProductDto.price;
if (updateProductDto.address) updateData.address = updateProductDto.address; if (updateProductDto.address) updateData.address = updateProductDto.address;
if (updateProductDto.status) updateData.status = updateProductDto.status;
if (updateProductDto.stock) updateData.stock = updateProductDto.stock; if (updateProductDto.stock) updateData.stock = updateProductDto.stock;
if (updateProductDto.urlImg) updateData.urlImg = updateProductDto.urlImg; if (updateProductDto.urlImg) updateData.urlImg = updateProductDto.urlImg;

View File

@@ -40,34 +40,36 @@ export default async function SurveyResponsePage({
return ( return (
// <PageContainer> // <PageContainer>
<main className='p-4 md:px-6 flex flex-col md:flex-row gap-4 md:relative'> <main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
<img <img
className="border-2 object-cover w-full h-full aspect-square rounded-2xl" className="border-2 object-cover w-full f-full min-h-[400px] md:h-[85vh] aspect-square rounded-2xl"
src={`http://localhost:3000/${product.urlImg}`} src={`http://localhost:3000/${product.urlImg}`}
alt="" alt=""
/> />
<Card className="flex flex-col md:w-[500px] md:max-h-[90vh] md:overflow-auto md:sticky top-0 right-0"> <Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0">
<CardHeader> <CardHeader className='py-2 px-2 md:px-4 lg:px-6'>
<CardTitle className="font-bold text-2xl"> <CardTitle className="font-bold text-2xl">
{product.title.charAt(0).toUpperCase() + product.title.slice(1)} {product.title.charAt(0).toUpperCase() + product.title.slice(1)}
</CardTitle> </CardTitle>
<p className='font-semibold'>$ {product.price}</p> <p className='font-semibold'>$ {product.price} </p>
{product.status === 'AGOTADO' ? (
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
): ('')}
</CardHeader> </CardHeader>
<CardContent className="flex-grow flex justify-between flex-col gap-4"> <CardContent className="py-0 px-2 md:px-4 lg:px-6 flex-col justify-between flex-grow md:overflow-auto">
<div> <div>
<p className='font-semibold text-lg border-t border-b'> Descripción</p> <p className='font-semibold text-lg border-t border-b'> Descripción</p>
<p className='p-1'>{product.description}</p> <p className='p-1'>{product.description}</p>
{/* <p className='p-1'>{lorem+lorem+lorem+lorem}</p> */} {/* <p className='p-1'>{lorem+lorem+lorem+lorem}</p> */}
</div> </div>
<div> <div className='mt-2'>
<p className='font-semibold text-lg border-t border-b'> Dirección</p> <p className='font-semibold text-lg border-t border-b'> Dirección</p>
<p>{product.address}</p> <p className='p-1'>{product.address}</p>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="">
<CardFooter className="px-2 md:px-4 lg:px-6">
<div> <div>
<p className='font-semibold text-lg border-t border-b mt-4'>Información del vendedor</p> <p className='font-semibold text-lg border-t border-b mt-4'>Información del vendedor</p>
<p>{product.fullname}</p> <p>{product.fullname}</p>

View File

@@ -0,0 +1,5 @@
export const STATUS = {
PUBLICADO:"Publicado",
AGOTADO:"Agotado",
BORRADOR:"Borrador",
}

View File

@@ -9,11 +9,19 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@repo/shadcn/form'; } from '@repo/shadcn/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { Textarea } from '@repo/shadcn/textarea'; import { Textarea } from '@repo/shadcn/textarea';
import { Input } from '@repo/shadcn/input'; import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation"; import { useCreateUser } from "@/feactures/inventory/hooks/use-mutation";
import { EditInventory, editInventory, formDataInput } from '@/feactures/inventory/schemas/inventory'; import { EditInventory, editInventory, formDataInput } from '@/feactures/inventory/schemas/inventory';
import {STATUS} from '@/constants/status'
interface CreateFormProps { interface CreateFormProps {
onSuccess?: () => void; onSuccess?: () => void;
@@ -34,9 +42,11 @@ export function CreateForm({
const defaultformValues = { const defaultformValues = {
title: '', title: '',
description: '', description: '',
address: '',
price: '', price: '',
stock: '', stock: '',
urlImg: '' urlImg: '',
status: ''
} }
const form = useForm<formDataInput>({ const form = useForm<formDataInput>({
@@ -100,6 +110,20 @@ export function CreateForm({
)} )}
/> />
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Dirección</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"
@@ -107,7 +131,6 @@ export function CreateForm({
<FormItem className='col-span-2'> <FormItem className='col-span-2'>
<FormLabel>Descripción</FormLabel> <FormLabel>Descripción</FormLabel>
<FormControl> <FormControl>
{/* <Input {...field} /> */}
<Textarea {...field} className="resize-none"/> <Textarea {...field} className="resize-none"/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -122,13 +145,36 @@ export function CreateForm({
<FormItem> <FormItem>
<FormLabel>Cantidad/Stock</FormLabel> <FormLabel>Cantidad/Stock</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} type='number' />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
<SelectContent>
{Object.entries(STATUS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="urlImg" name="urlImg"

View File

@@ -22,6 +22,8 @@ export const columns: ColumnDef<InventoryTable>[] = [
{ {
accessorKey: "description", accessorKey: "description",
header: "Descripcion", header: "Descripcion",
cell: ({ row }) => row.original.description.length > 40 ?
`${row.original.description.slice(0, 40)}...` : row.original.description
}, },
{ {
accessorKey: 'price', accessorKey: 'price',
@@ -32,7 +34,10 @@ export const columns: ColumnDef<InventoryTable>[] = [
accessorKey: 'stock', accessorKey: 'stock',
header: 'Stock', header: 'Stock',
}, },
{
accessorKey: 'status',
header: 'Estado',
},
{ {
id: 'actions', id: 'actions',
header: 'Acciones', header: 'Acciones',

View File

@@ -9,11 +9,19 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '@repo/shadcn/form'; } from '@repo/shadcn/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@repo/shadcn/select';
import { Input } from '@repo/shadcn/input'; import { Input } from '@repo/shadcn/input';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation"; import { useUpdateUser } from "@/feactures/inventory/hooks/use-mutation";
import { editInventory, formDataInput, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad import { editInventory, formDataInput, EditInventory } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
import { Textarea } from '@repo/shadcn/components/ui/textarea'; import { Textarea } from '@repo/shadcn/components/ui/textarea';
import {STATUS} from '@/constants/status'
interface UpdateFormProps { interface UpdateFormProps {
onSuccess?: () => void; onSuccess?: () => void;
@@ -37,6 +45,8 @@ export function UpdateForm({
title: defaultValues?.title || '', title: defaultValues?.title || '',
description: defaultValues?.description || '', description: defaultValues?.description || '',
price: defaultValues?.price || '', price: defaultValues?.price || '',
address: defaultValues?.address || '',
status: defaultValues?.status || 'BORRADOR',
stock: (defaultValues?.stock ?? '').toString(), stock: (defaultValues?.stock ?? '').toString(),
urlImg: defaultValues?.urlImg || '', urlImg: defaultValues?.urlImg || '',
userId: defaultValues?.userId userId: defaultValues?.userId
@@ -49,6 +59,7 @@ export function UpdateForm({
}); });
const onSubmit = async (data: formDataInput) => { const onSubmit = async (data: formDataInput) => {
console.log(data);
saveAccountingAccounts(data, { saveAccountingAccounts(data, {
onSuccess: () => { onSuccess: () => {
@@ -88,13 +99,11 @@ export function UpdateForm({
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="price" name="price"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem >
<FormLabel>Precio</FormLabel> <FormLabel>Precio</FormLabel>
<FormControl> <FormControl>
{/* Simplificado. price es z.string(), field.value ya es string o undefined. */} {/* Simplificado. price es z.string(), field.value ya es string o undefined. */}
@@ -105,6 +114,21 @@ export function UpdateForm({
)} )}
/> />
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className='col-span-2'>
<FormLabel>Dirección</FormLabel>
<FormControl>
{/* Simplificado. price es z.string(), field.value ya es string o undefined. */}
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"
@@ -112,7 +136,6 @@ export function UpdateForm({
<FormItem className='col-span-2'> <FormItem className='col-span-2'>
<FormLabel>Descripción</FormLabel> <FormLabel>Descripción</FormLabel>
<FormControl> <FormControl>
{/* <Input {...field} /> */}
<Textarea {...field} className="resize-none"/> <Textarea {...field} className="resize-none"/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -127,7 +150,6 @@ export function UpdateForm({
<FormItem> <FormItem>
<FormLabel>Cantidad/Stock</FormLabel> <FormLabel>Cantidad/Stock</FormLabel>
<FormControl> <FormControl>
{/* Añadido type="number" para UX. field.value ya es string debido a formDataInput */}
<Input {...field} type="number" /> <Input {...field} type="number" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -135,6 +157,29 @@ export function UpdateForm({
)} )}
/> />
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Estatus</FormLabel>
<Select value={field.value} onValueChange={(value) => field.onChange(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione un estatus" />
</SelectTrigger>
<SelectContent>
{Object.entries(STATUS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="urlImg" name="urlImg"

View File

@@ -42,12 +42,15 @@ export function ProductList() {
</CardHeader> </CardHeader>
<CardContent className="p-0 flex-grow"> <CardContent className="p-0 flex-grow">
<img <img
className="object-cover w-full h-full aspect-square" className="object-cover w-full h-full aspect-square border"
src={`http://localhost:3000/${data.urlImg}`} src={`http://localhost:3000/${data.urlImg}`}
alt="" alt=""
/> />
</CardContent> </CardContent>
<CardFooter className="flex justify-between items-center p-4"> <CardFooter className="flex-col items-start p-4">
{data.status === 'AGOTADO' ? (
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
): ('')}
<p className="font-semibold text-lg">$ {data.price}</p> <p className="font-semibold text-lg">$ {data.price}</p>
</CardFooter> </CardFooter>
</Card> </Card>

View File

@@ -17,6 +17,7 @@ export const product = z.object({
stock: z.number(), stock: z.number(),
price: z.string(), price: z.string(),
urlImg: z.string(), urlImg: z.string(),
status: z.string(),
userId: z.number().optional() userId: z.number().optional()
}) })
@@ -32,9 +33,11 @@ export const editInventory = z.object({
description: z.string().min(10, { message: "Debe de tener 10 o más caracteres" }), description: z.string().min(10, { message: "Debe de tener 10 o más caracteres" }),
stock: z.string().transform(val => Number(val)).pipe(z.number( stock: z.string().transform(val => Number(val)).pipe(z.number(
{ invalid_type_error: 'El stock debe ser un número' }).min(0, { message: "El stock debe ser mayor a 0" }) { invalid_type_error: 'El stock debe ser un número' }).min(0, { message: "El stock debe ser mayor a 0" })
), ),
address: z.string().min(5, { message: "Debe de tener 5 o más caracteres" }),
price: z.string(), price: z.string(),
urlImg: z.string(), urlImg: z.string(),
status: z.string().min(1, { message: "Debe de seleccionar un valor" }),
userId: z.number().optional(), userId: z.number().optional(),
}) })

BIN
apps/web/public/apples.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
'border-input placeholder:text-muted-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive dark:aria-invalid:ring-destructive/50 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4', 'border-gray-400 dark:border-input placeholder:text-muted-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive dark:aria-invalid:ring-destructive/50 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4',
className, className,
)} )}
{...props} {...props}