mirror of
https://github.com/borbann-platform/backend-api.git
synced 2025-12-18 12:14:05 +01:00
feat: implement multi-step form for creating data pipelines, refactor existing components for improved structure and styling
This commit is contained in:
parent
a687636b07
commit
42b9fdedae
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user