Initial commit: LeadFlow lead generation platform
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>
This commit is contained in:
68
components/shared/FileDropZone.tsx
Normal file
68
components/shared/FileDropZone.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Upload, FileText } from "lucide-react";
|
||||
|
||||
interface FileDropZoneProps {
|
||||
onFile: (content: string, filename: string) => void;
|
||||
accept?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function FileDropZone({ onFile, accept = ".csv", label = "Drop your CSV file here" }: FileDropZoneProps) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [filename, setFilename] = useState<string | null>(null);
|
||||
|
||||
const processFile = useCallback((file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
onFile(e.target?.result as string, file.name);
|
||||
setFilename(file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}, [onFile]);
|
||||
|
||||
const onDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) processFile(file);
|
||||
}, [processFile]);
|
||||
|
||||
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) processFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"relative flex flex-col items-center justify-center w-full h-40 border-2 border-dashed rounded-xl cursor-pointer transition-all",
|
||||
dragging
|
||||
? "border-blue-500 bg-blue-500/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
|
||||
: filename
|
||||
? "border-green-500/50 bg-green-500/5"
|
||||
: "border-[#2e2e3e] bg-[#0d0d18] hover:border-blue-500/50 hover:bg-blue-500/5"
|
||||
)}
|
||||
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<input type="file" accept={accept} className="sr-only" onChange={onInputChange} />
|
||||
{filename ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<FileText className="w-8 h-8 text-green-400" />
|
||||
<span className="text-sm text-green-400 font-medium">{filename}</span>
|
||||
<span className="text-xs text-gray-500">Click to replace</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<Upload className="w-8 h-8 text-gray-500" />
|
||||
<span className="text-sm text-gray-300">{label}</span>
|
||||
<span className="text-xs text-gray-500">Click to browse or drag & drop</span>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user