Full-stack Next.js 16 app with three scraping pipelines: - AirScale CSV → Anymailfinder Bulk Decision Maker search - LinkedIn Sales Navigator → Vayne → Anymailfinder email enrichment - Apify Google SERP → domain extraction → Anymailfinder bulk enrichment Includes Docker multi-stage build + docker-compose for Coolify deployment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, KeyboardEvent } from "react";
|
|
import { X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface RoleChipsInputProps {
|
|
value: string[];
|
|
onChange: (roles: string[]) => void;
|
|
label?: string;
|
|
}
|
|
|
|
export function RoleChipsInput({ value, onChange, label = "Target Roles" }: RoleChipsInputProps) {
|
|
const [input, setInput] = useState("");
|
|
|
|
const add = (role: string) => {
|
|
const trimmed = role.trim();
|
|
if (trimmed && !value.includes(trimmed)) {
|
|
onChange([...value, trimmed]);
|
|
}
|
|
setInput("");
|
|
};
|
|
|
|
const remove = (role: string) => onChange(value.filter(r => r !== role));
|
|
|
|
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
if (e.key === "Enter" || e.key === ",") {
|
|
e.preventDefault();
|
|
add(input);
|
|
} else if (e.key === "Backspace" && !input && value.length > 0) {
|
|
remove(value[value.length - 1]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{label && <label className="block text-sm font-medium text-gray-300 mb-2">{label}</label>}
|
|
<div className="min-h-[44px] flex flex-wrap gap-2 p-2.5 bg-[#0d0d18] border border-[#2e2e3e] rounded-lg focus-within:border-blue-500 transition-colors">
|
|
{value.map(role => (
|
|
<span
|
|
key={role}
|
|
className="flex items-center gap-1 bg-blue-500/20 text-blue-300 border border-blue-500/30 rounded-md px-2.5 py-0.5 text-sm"
|
|
>
|
|
{role}
|
|
<button onClick={() => remove(role)} className="hover:text-white transition-colors">
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
<input
|
|
className="flex-1 min-w-[120px] bg-transparent text-sm text-white outline-none placeholder:text-gray-600"
|
|
placeholder="Add role, press Enter..."
|
|
value={input}
|
|
onChange={e => setInput(e.target.value)}
|
|
onKeyDown={onKeyDown}
|
|
onBlur={() => input && add(input)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|