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 { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import PageHeader from "@/components/page-header"; import CratePipelineForm from "./create-pipeline-multiform";
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";
export default function CreatePipelinePage() { export default function CreatePipelinePage() {
return ( return (
<div className="container mx-auto p-6"> <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"> <div className="mt-6">
<Link href="/data-pipeline"> <Link href="/data-pipeline">
<Button variant="outline" className="mb-6"> <Button variant="outline" className="mb-6">
@ -27,21 +13,10 @@ export default function CreatePipelinePage() {
Back to Pipelines Back to Pipelines
</Button> </Button>
</Link> </Link>
<CratePipelineForm />
<div className="grid gap-6 md:grid-cols-2">
<div>
<PipelineDetails />
<PipelineAiAssistant />
</div>
<AddDataSource />
<div>
<ScheduleAndInformation />
</div>
</div>
<div className="mt-6 flex justify-end space-x-4"> <div className="mt-6 flex justify-end space-x-4">
<Button variant="outline">Save as Draft</Button> {/* <Button variant="outline">Save as Draft</Button>
<Button>Create Pipeline</Button> <Button>Create Pipeline</Button> */}
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,8 @@ import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "../ui/accordion"; } from "../ui/accordion";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { import {
Card, Card,
CardContent, CardContent,
@ -12,15 +14,13 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../ui/card"; } from "../ui/card";
import { Label } from "../ui/label";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { Badge } from "../ui/badge"; import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
export function AddDataSource() { export function AddDataSource() {
return ( 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> <CardHeader>
<CardTitle>Data Sources</CardTitle> <CardTitle>Data Sources</CardTitle>
<CardDescription> <CardDescription>

View File

@ -1,22 +1,19 @@
import { Switch } from "../ui/switch"; import {
import { BrainCircuit } from "lucide-react"; Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
export function PipelineAiAssistant(){ export function PipelineAiAssistant() {
return ( 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> <CardHeader>
<div> <CardTitle>AI Assistant</CardTitle>
<CardTitle className="text-lg flex items-center"> <CardDescription>Customize how AI processes your data</CardDescription>
<BrainCircuit className="mr-2 h-5 w-5 text-primary" />
AI Assistant
</CardTitle>
<CardDescription>
Customize how AI processes your data
</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
@ -33,13 +30,11 @@ export function PipelineAiAssistant(){
data sources data sources
</p> </p>
</div> </div>
{/*
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="detect-fields"> <Label htmlFor="detect-fields">Auto-detect common fields</Label>
Auto-detect common fields
</Label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Automatically identify price, location, etc. Automatically identify price, location, etc.
</p> </p>
@ -49,9 +44,7 @@ export function PipelineAiAssistant(){
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="suggest-mappings"> <Label htmlFor="suggest-mappings">Suggest field mappings</Label>
Suggest field mappings
</Label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Get AI suggestions for matching fields across sources Get AI suggestions for matching fields across sources
</p> </p>
@ -68,7 +61,7 @@ export function PipelineAiAssistant(){
</div> </div>
<Switch id="deduplicate" defaultChecked /> <Switch id="deduplicate" defaultChecked />
</div> </div>
</div> </div> */}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,11 +1,17 @@
import { Label } from "../ui/label"; import {
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card"; Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea"; import { Textarea } from "../ui/textarea";
export function PipelineDetails(){ export function PipelineDetails() {
return ( 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> <CardHeader>
<CardTitle>Pipeline Details</CardTitle> <CardTitle>Pipeline Details</CardTitle>
<CardDescription> <CardDescription>

View File

@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
export function ScheduleAndInformation() { export function ScheduleAndInformation() {
return ( 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> <CardHeader>
<CardTitle>Schedule & Automation</CardTitle> <CardTitle>Schedule & Automation</CardTitle>
<CardDescription> <CardDescription>

View File

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

View File

@ -123,6 +123,9 @@ dependencies:
embla-carousel-react: embla-carousel-react:
specifier: 8.5.1 specifier: 8.5.1
version: 8.5.1(react@19.0.0) 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: input-otp:
specifier: 1.4.1 specifier: 1.4.1
version: 1.4.1(react-dom@19.0.0)(react@19.0.0) version: 1.4.1(react-dom@19.0.0)(react@19.0.0)
@ -3385,6 +3388,27 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: false 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: /function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: true dev: true
@ -4035,6 +4059,16 @@ packages:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true 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: /ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true dev: true