Components
Loading preview...
Async Select component built with React & shadcn/ui
npx shadcn@latest add https://21st.dev/r/rudrodip/async-select"use client";
import React, { useState } from "react";
import Image from "next/image";
import { AsyncSelect } from "@/components/ui/async-select";
// Types
interface User {
id: string;
name: string;
role: string;
avatar: string;
email: string;
}
// Mock data
const mockUsers: User[] = [
{
id: "1",
name: "John Doe",
role: "Software Engineer",
email: "john@example.com",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=John",
},
{
id: "2",
name: "Jane Smith",
role: "Product Manager",
email: "jane@example.com",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Jane",
},
{
id: "3",
name: "Bob Johnson",
role: "Designer",
email: "bob@example.com",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Bob",
},
{
id: "4",
name: "Alice Brown",
role: "Developer",
email: "alice@example.com",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice",
},
];
// Mock API call
const searchUsers = async (query?: string): Promise<User[]> => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
if (!query) return mockUsers;
return mockUsers.filter(user =>
user.name.toLowerCase().includes(query.toLowerCase()) ||
user.role.toLowerCase().includes(query.toLowerCase()) ||
user.email.toLowerCase().includes(query.toLowerCase())
);
};
// Basic Demo
function BasicAsyncSelectDemo() {
const [selectedUser, setSelectedUser] = useState("");
return (
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Basic Usage</h3>
<AsyncSelect<User>
fetcher={searchUsers}
renderOption={(user) => (
<div className="flex items-center gap-2">
<Image
src={user.avatar}
alt={user.name}
width={24}
height={24}
className="rounded-full"
/>
<div className="flex flex-col">
<div className="font-medium">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.role}</div>
</div>
</div>
)}
getOptionValue={(user) => user.id}
getDisplayValue={(user) => (
<div className="flex items-center gap-2 text-left">
<Image
src={user.avatar}
alt={user.name}
width={24}
height={24}
className="rounded-full"
/>
<div className="flex flex-col leading-tight">
<div className="font-medium">{user.name}</div>
<div className="text-xxs text-muted-foreground">{user.role}</div>
</div>
</div>
)}
notFound={<div className="py-6 text-center text-sm">No users found</div>}
label="User"
placeholder="Search users..."
value={selectedUser}
onChange={setSelectedUser}
width="375px"
/>
<p className="text-sm text-muted-foreground">
Selected user ID: {selectedUser || "none"}
</p>
</div>
);
}
// Preloaded Demo
function PreloadedAsyncSelectDemo() {
const [selectedUser, setSelectedUser] = useState("");
return (
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Preloaded Data</h3>
<AsyncSelect<User>
fetcher={searchUsers}
preload={true}
filterFn={(user, query) =>
user.name.toLowerCase().includes(query.toLowerCase()) ||
user.role.toLowerCase().includes(query.toLowerCase())
}
renderOption={(user) => (
<div className="flex items-center gap-2">
<Image
src={user.avatar}
alt={user.name}
width={24}
height={24}
className="rounded-full"
/>
<div className="flex flex-col">
<div className="font-medium">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.email}</div>
</div>
</div>
)}
getOptionValue={(user) => user.id}
getDisplayValue={(user) => user.name}
label="User"
placeholder="Select user..."
value={selectedUser}
onChange={setSelectedUser}
width="300px"
/>
</div>
);
}
// Disabled Demo
function DisabledAsyncSelectDemo() {
const [selectedUser, setSelectedUser] = useState("");
return (
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Disabled State</h3>
<AsyncSelect<User>
fetcher={searchUsers}
renderOption={(user) => user.name}
getOptionValue={(user) => user.id}
getDisplayValue={(user) => user.name}
label="User"
placeholder="Select user..."
value={selectedUser}
onChange={setSelectedUser}
disabled={true}
width="250px"
/>
</div>
);
}
// Custom Loading Demo
function CustomLoadingAsyncSelectDemo() {
const [selectedUser, setSelectedUser] = useState("");
return (
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Custom Loading State</h3>
<AsyncSelect<User>
fetcher={searchUsers}
renderOption={(user) => user.name}
getOptionValue={(user) => user.id}
getDisplayValue={(user) => user.name}
loadingSkeleton={
<div className="p-4 text-center">
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full mx-auto" />
<p className="mt-2 text-sm text-muted-foreground">Loading users...</p>
</div>
}
label="User"
placeholder="Select user..."
value={selectedUser}
onChange={setSelectedUser}
width="250px"
/>
</div>
);
}
export { BasicAsyncSelectDemo, PreloadedAsyncSelectDemo, DisabledAsyncSelectDemo, CustomLoadingAsyncSelectDemo }