feat: implement multi-step form for creating data pipelines, refactor existing components for improved structure and styling

This commit is contained in:
THIS ONE IS A LITTLE BIT TRICKY KRUB 2025-04-25 18:15:26 +07:00
parent a687636b07
commit 42b9fdedae
8 changed files with 282 additions and 143 deletions

View File

@ -0,0 +1,130 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { AddDataSource } from "@/components/pipeline/add-data-source";
import { PipelineAiAssistant } from "@/components/pipeline/ai-assistant";
import { PipelineDetails } from "@/components/pipeline/details";
import { ScheduleAndInformation } from "@/components/pipeline/schedule-and-information";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { cn } from "@/lib/utils";
const TOTAL_STEPS = 5;
const stepComponents = [
<PipelineDetails key="details" />,
<AddDataSource key="datasource" />,
<PipelineAiAssistant key="ai" />,
<ScheduleAndInformation key="schedule" />,
<div
key="inputs"
className="border border-dashed rounded-md flex flex-col items-center justify-center h-[8rem]"
>
<h3 className="text-base font-semibold text-center">
No Inputs Added Yet!
</h3>
<p className="text-xs text-muted-foreground text-center">
Start building your form by adding input fields.
</p>
</div>,
];
const motionVariants = {
enter: { opacity: 0, x: 50 },
center: { opacity: 1, x: 0 },
exit: { opacity: 0, x: -50 },
};
export default function CratePipelineForm() {
const [step, setStep] = useState(0);
const isFirstStep = step === 0;
const isLastStep = step === TOTAL_STEPS - 1;
const form = useForm();
const { handleSubmit, reset } = form;
const onSubmit = async (formData: unknown) => {
if (!isLastStep) return setStep((s) => s + 1);
console.log(formData);
reset();
setStep(0);
toast.success("Form successfully submitted");
};
const handleBack = () => setStep((s) => (s > 0 ? s - 1 : s));
const StepForm = (
<Form {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-y-4">
{stepComponents[step]}
<div className="flex justify-between">
<Button
type="button"
size="sm"
className="font-medium cursor-pointer"
onClick={handleBack}
disabled={isFirstStep}
>
Back
</Button>
<Button
type="submit"
size="sm"
className="font-medium cursor-pointer"
>
{isLastStep ? "Create Pipeline" : "Next"}
</Button>
</div>
</form>
</Form>
);
return (
<div className="space-y-4">
{/* stepper */}
<div className="flex items-center justify-center">
{Array.from({ length: TOTAL_STEPS }).map((_, index) => (
<div key={index} className="flex items-center">
<div
className={cn(
"w-4 h-4 rounded-full transition-all duration-300 ease-in-out",
index <= step ? "bg-primary" : "bg-primary/30"
)}
/>
{index < TOTAL_STEPS - 1 && (
<div
className={cn(
"w-8 h-0.5",
index < step ? "bg-primary" : "bg-primary/30"
)}
/>
)}
</div>
))}
</div>
{/* animated form */}
<Card className="shadow-sm">
<CardContent>
<AnimatePresence mode="wait">
<motion.div
key={step}
variants={motionVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.1 }}
>
{StepForm}
</motion.div>
</AnimatePresence>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,25 +1,11 @@
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import PageHeader from "@/components/page-header";
import { PipelineDetails } from "@/components/pipeline/details";
import { PipelineAiAssistant } from "@/components/pipeline/ai-assistant";
import { AddDataSource } from "@/components/pipeline/add-data-source";
import { ScheduleAndInformation } from "@/components/pipeline/schedule-and-information";
import CratePipelineForm from "./create-pipeline-multiform";
export default function CreatePipelinePage() {
return (
<div className="container mx-auto p-6">
<PageHeader
title="Create Data Pipeline"
description="Set up a new automated data collection pipeline"
breadcrumb={[
{ title: "Home", href: "/" },
{ title: "Data Pipeline", href: "/data-pipeline" },
{ title: "Create", href: "/data-pipeline/create" },
]}
/>
<div className="mt-6">
<Link href="/data-pipeline">
<Button variant="outline" className="mb-6">
@ -27,21 +13,10 @@ export default function CreatePipelinePage() {
Back to Pipelines
</Button>
</Link>
<div className="grid gap-6 md:grid-cols-2">
<div>
<PipelineDetails />
<PipelineAiAssistant />
</div>
<AddDataSource />
<div>
<ScheduleAndInformation />
</div>
</div>
<CratePipelineForm />
<div className="mt-6 flex justify-end space-x-4">
<Button variant="outline">Save as Draft</Button>
<Button>Create Pipeline</Button>
{/* <Button variant="outline">Save as Draft</Button>
<Button>Create Pipeline</Button> */}
</div>
</div>
</div>

View File

@ -5,6 +5,8 @@ import {
AccordionItem,
AccordionTrigger,
} from "../ui/accordion";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Card,
CardContent,
@ -12,15 +14,13 @@ import {
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
export function AddDataSource() {
return (
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<Card className="border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Data Sources</CardTitle>
<CardDescription>

View File

@ -1,76 +1,69 @@
import { Switch } from "../ui/switch";
import { BrainCircuit } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card";
import { Textarea } from "../ui/textarea";
export function PipelineAiAssistant(){
return (
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<div>
<CardTitle className="text-lg flex items-center">
<BrainCircuit className="mr-2 h-5 w-5 text-primary" />
AI Assistant
</CardTitle>
<CardDescription>
Customize how AI processes your data
</CardDescription>
export function PipelineAiAssistant() {
return (
<Card className="mt-6 border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>AI Assistant</CardTitle>
<CardDescription>Customize how AI processes your data</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ai-prompt">Additional Instructions for AI</Label>
<Textarea
id="ai-prompt"
placeholder="E.g., Focus on extracting pricing trends, ignore promotional content, prioritize property features..."
rows={4}
className="border-primary/20"
/>
<p className="text-xs text-muted-foreground mt-1">
Provide specific instructions to guide the AI in processing your
data sources
</p>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="ai-prompt">Additional Instructions for AI</Label>
<Textarea
id="ai-prompt"
placeholder="E.g., Focus on extracting pricing trends, ignore promotional content, prioritize property features..."
rows={4}
className="border-primary/20"
/>
<p className="text-xs text-muted-foreground mt-1">
Provide specific instructions to guide the AI in processing your
data sources
</p>
{/*
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="detect-fields">Auto-detect common fields</Label>
<p className="text-xs text-muted-foreground">
Automatically identify price, location, etc.
</p>
</div>
<Switch id="detect-fields" defaultChecked />
</div>
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="detect-fields">
Auto-detect common fields
</Label>
<p className="text-xs text-muted-foreground">
Automatically identify price, location, etc.
</p>
</div>
<Switch id="detect-fields" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="suggest-mappings">
Suggest field mappings
</Label>
<p className="text-xs text-muted-foreground">
Get AI suggestions for matching fields across sources
</p>
</div>
<Switch id="suggest-mappings" defaultChecked />
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="deduplicate">Deduplicate records</Label>
<p className="text-xs text-muted-foreground">
Remove duplicate entries automatically
</p>
</div>
<Switch id="deduplicate" defaultChecked />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="suggest-mappings">Suggest field mappings</Label>
<p className="text-xs text-muted-foreground">
Get AI suggestions for matching fields across sources
</p>
</div>
<Switch id="suggest-mappings" defaultChecked />
</div>
</div>
</CardContent>
</Card>
);
}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="deduplicate">Deduplicate records</Label>
<p className="text-xs text-muted-foreground">
Remove duplicate entries automatically
</p>
</div>
<Switch id="deduplicate" defaultChecked />
</div>
</div> */}
</div>
</CardContent>
</Card>
);
}

View File

@ -1,45 +1,51 @@
import { Label } from "../ui/label";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
export function PipelineDetails(){
return (
<Card className="border-2 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Pipeline Details</CardTitle>
<CardDescription>
Basic information about your data pipeline
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Pipeline Name</Label>
<Input id="name" placeholder="e.g., Property Listings Pipeline" />
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe what this pipeline collects and how it will be used"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="tags">Tags (optional)</Label>
<Input
id="tags"
placeholder="e.g., real-estate, properties, listings"
/>
<p className="text-xs text-muted-foreground mt-1">
Separate tags with commas
</p>
</div>
export function PipelineDetails() {
return (
<Card className="border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Pipeline Details</CardTitle>
<CardDescription>
Basic information about your data pipeline
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Pipeline Name</Label>
<Input id="name" placeholder="e.g., Property Listings Pipeline" />
</div>
</CardContent>
</Card>
);
}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe what this pipeline collects and how it will be used"
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="tags">Tags (optional)</Label>
<Input
id="tags"
placeholder="e.g., real-estate, properties, listings"
/>
<p className="text-xs text-muted-foreground mt-1">
Separate tags with commas
</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
export function ScheduleAndInformation() {
return (
<Card className="mt-6 border-2 hover:border-highlight-border transition-all duration-200">
<Card className="mt-6 border-0 hover:border-highlight-border transition-all duration-200">
<CardHeader>
<CardTitle>Schedule & Automation</CardTitle>
<CardDescription>

View File

@ -47,6 +47,7 @@
"cmdk": "1.1.1",
"date-fns": "4.1.0",
"embla-carousel-react": "8.5.1",
"framer-motion": "^12.9.1",
"input-otp": "1.4.1",
"lucide-react": "^0.487.0",
"next": "15.2.1",

View File

@ -123,6 +123,9 @@ dependencies:
embla-carousel-react:
specifier: 8.5.1
version: 8.5.1(react@19.0.0)
framer-motion:
specifier: ^12.9.1
version: 12.9.1(react-dom@19.0.0)(react@19.0.0)
input-otp:
specifier: 1.4.1
version: 1.4.1(react-dom@19.0.0)(react@19.0.0)
@ -3385,6 +3388,27 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: false
/framer-motion@12.9.1(react-dom@19.0.0)(react@19.0.0):
resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
dependencies:
motion-dom: 12.9.1
motion-utils: 12.8.3
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
tslib: 2.8.1
dev: false
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: true
@ -4035,6 +4059,16 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/motion-dom@12.9.1:
resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==}
dependencies:
motion-utils: 12.8.3
dev: false
/motion-utils@12.8.3:
resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true