Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,262 +1,262 @@
'use client'
import { motion, AnimatePresence } from 'motion/react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { CheckCircle2, AlertCircle, Loader2, Save, Rocket } from 'lucide-react'
export type StepConfig = {
title: string
description?: string
isValid?: boolean
hasErrors?: boolean
}
type SidebarStepperProps = {
steps: StepConfig[]
currentStep: number
onStepChange: (index: number) => void
onSave?: () => void
onSubmit?: () => void
isSaving?: boolean
isSubmitting?: boolean
saveLabel?: string
submitLabel?: string
canSubmit?: boolean
children: React.ReactNode
className?: string
}
export function SidebarStepper({
steps,
currentStep,
onStepChange,
onSave,
onSubmit,
isSaving = false,
isSubmitting = false,
saveLabel = 'Save Draft',
submitLabel = 'Create Pipeline',
canSubmit = true,
children,
className,
}: SidebarStepperProps) {
const direction = (prev: number, next: number) => (next > prev ? 1 : -1)
return (
<div className={cn('flex gap-6 min-h-[600px]', className)}>
{/* Sidebar - hidden on mobile */}
<div className="hidden lg:flex lg:flex-col lg:w-[260px] lg:shrink-0">
<nav className="flex-1 space-y-1 py-2">
{steps.map((step, index) => {
const isCurrent = index === currentStep
const isComplete = step.isValid === true
const hasErrors = step.hasErrors === true
return (
<button
key={index}
type="button"
onClick={() => onStepChange(index)}
className={cn(
'w-full flex items-start gap-3 rounded-lg px-3 py-2.5 text-left transition-colors',
isCurrent
? 'bg-primary/5 border border-primary/20'
: 'hover:bg-muted/50 border border-transparent'
)}
>
<div
className={cn(
'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-colors',
isCurrent && 'bg-primary text-primary-foreground',
!isCurrent && isComplete && 'bg-emerald-500 text-white',
!isCurrent && hasErrors && 'bg-destructive/10 text-destructive border border-destructive/30',
!isCurrent && !isComplete && !hasErrors && 'bg-muted text-muted-foreground'
)}
>
{isComplete && !isCurrent ? (
<CheckCircle2 className="h-3.5 w-3.5" />
) : hasErrors && !isCurrent ? (
<AlertCircle className="h-3.5 w-3.5" />
) : (
index + 1
)}
</div>
<div className="min-w-0 flex-1">
<p
className={cn(
'text-sm font-medium truncate',
isCurrent && 'text-primary',
!isCurrent && 'text-foreground'
)}
>
{step.title}
</p>
{step.description && (
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
{step.description}
</p>
)}
</div>
</button>
)
})}
</nav>
{/* Actions */}
<div className="border-t pt-4 space-y-2 mt-auto">
{onSave && (
<Button
variant="outline"
className="w-full justify-start"
disabled={isSaving || isSubmitting}
onClick={onSave}
>
{isSaving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
{saveLabel}
</Button>
)}
{onSubmit && (
<Button
className="w-full justify-start"
disabled={isSubmitting || isSaving || !canSubmit}
onClick={onSubmit}
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Rocket className="h-4 w-4 mr-2" />
)}
{submitLabel}
</Button>
)}
</div>
</div>
{/* Mobile step indicator */}
<div className="lg:hidden flex flex-col w-full">
<MobileStepIndicator
steps={steps}
currentStep={currentStep}
onStepChange={onStepChange}
/>
<div className="flex-1 mt-4">
<StepContent currentStep={currentStep} direction={direction}>
{children}
</StepContent>
</div>
{/* Mobile actions */}
<div className="flex gap-2 pt-4 border-t mt-4">
{onSave && (
<Button
variant="outline"
size="sm"
className="flex-1"
disabled={isSaving || isSubmitting}
onClick={onSave}
>
{isSaving ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
{saveLabel}
</Button>
)}
{onSubmit && (
<Button
size="sm"
className="flex-1"
disabled={isSubmitting || isSaving || !canSubmit}
onClick={onSubmit}
>
{isSubmitting ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Rocket className="h-4 w-4 mr-1" />}
{submitLabel}
</Button>
)}
</div>
</div>
{/* Desktop content */}
<div className="hidden lg:block flex-1 min-w-0">
<StepContent currentStep={currentStep} direction={direction}>
{children}
</StepContent>
</div>
</div>
)
}
function MobileStepIndicator({
steps,
currentStep,
onStepChange,
}: {
steps: StepConfig[]
currentStep: number
onStepChange: (index: number) => void
}) {
return (
<div className="overflow-x-auto">
<div className="flex items-center gap-1 pb-2 min-w-max">
{steps.map((step, index) => {
const isCurrent = index === currentStep
const isComplete = step.isValid === true
return (
<button
key={index}
type="button"
onClick={() => onStepChange(index)}
className={cn(
'flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium transition-colors shrink-0',
isCurrent && 'bg-primary text-primary-foreground',
!isCurrent && isComplete && 'bg-emerald-100 text-emerald-700',
!isCurrent && !isComplete && 'bg-muted text-muted-foreground hover:bg-muted/80'
)}
>
{isComplete && !isCurrent ? (
<CheckCircle2 className="h-3 w-3" />
) : (
<span>{index + 1}</span>
)}
<span className="hidden sm:inline">{step.title}</span>
</button>
)
})}
</div>
</div>
)
}
function StepContent({
currentStep,
direction,
children,
}: {
currentStep: number
direction: (prev: number, next: number) => number
children: React.ReactNode
}) {
const childArray = Array.isArray(children) ? children : [children]
const currentChild = childArray[currentStep]
return (
<div className="relative overflow-hidden">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.2,
ease: 'easeInOut',
}}
>
{currentChild}
</motion.div>
</AnimatePresence>
</div>
)
}
'use client'
import { motion, AnimatePresence } from 'motion/react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { CheckCircle2, AlertCircle, Loader2, Save, Rocket } from 'lucide-react'
export type StepConfig = {
title: string
description?: string
isValid?: boolean
hasErrors?: boolean
}
type SidebarStepperProps = {
steps: StepConfig[]
currentStep: number
onStepChange: (index: number) => void
onSave?: () => void
onSubmit?: () => void
isSaving?: boolean
isSubmitting?: boolean
saveLabel?: string
submitLabel?: string
canSubmit?: boolean
children: React.ReactNode
className?: string
}
export function SidebarStepper({
steps,
currentStep,
onStepChange,
onSave,
onSubmit,
isSaving = false,
isSubmitting = false,
saveLabel = 'Save Draft',
submitLabel = 'Create Pipeline',
canSubmit = true,
children,
className,
}: SidebarStepperProps) {
const direction = (prev: number, next: number) => (next > prev ? 1 : -1)
return (
<div className={cn('flex gap-6 min-h-[600px]', className)}>
{/* Sidebar - hidden on mobile */}
<div className="hidden lg:flex lg:flex-col lg:w-[260px] lg:shrink-0">
<nav className="flex-1 space-y-1 py-2">
{steps.map((step, index) => {
const isCurrent = index === currentStep
const isComplete = step.isValid === true
const hasErrors = step.hasErrors === true
return (
<button
key={index}
type="button"
onClick={() => onStepChange(index)}
className={cn(
'w-full flex items-start gap-3 rounded-lg px-3 py-2.5 text-left transition-colors',
isCurrent
? 'bg-primary/5 border border-primary/20'
: 'hover:bg-muted/50 border border-transparent'
)}
>
<div
className={cn(
'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-colors',
isCurrent && 'bg-primary text-primary-foreground',
!isCurrent && isComplete && 'bg-emerald-500 text-white',
!isCurrent && hasErrors && 'bg-destructive/10 text-destructive border border-destructive/30',
!isCurrent && !isComplete && !hasErrors && 'bg-muted text-muted-foreground'
)}
>
{isComplete && !isCurrent ? (
<CheckCircle2 className="h-3.5 w-3.5" />
) : hasErrors && !isCurrent ? (
<AlertCircle className="h-3.5 w-3.5" />
) : (
index + 1
)}
</div>
<div className="min-w-0 flex-1">
<p
className={cn(
'text-sm font-medium truncate',
isCurrent && 'text-primary',
!isCurrent && 'text-foreground'
)}
>
{step.title}
</p>
{step.description && (
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
{step.description}
</p>
)}
</div>
</button>
)
})}
</nav>
{/* Actions */}
<div className="border-t pt-4 space-y-2 mt-auto">
{onSave && (
<Button
variant="outline"
className="w-full justify-start"
disabled={isSaving || isSubmitting}
onClick={onSave}
>
{isSaving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
{saveLabel}
</Button>
)}
{onSubmit && (
<Button
className="w-full justify-start"
disabled={isSubmitting || isSaving || !canSubmit}
onClick={onSubmit}
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Rocket className="h-4 w-4 mr-2" />
)}
{submitLabel}
</Button>
)}
</div>
</div>
{/* Mobile step indicator */}
<div className="lg:hidden flex flex-col w-full">
<MobileStepIndicator
steps={steps}
currentStep={currentStep}
onStepChange={onStepChange}
/>
<div className="flex-1 mt-4">
<StepContent currentStep={currentStep} direction={direction}>
{children}
</StepContent>
</div>
{/* Mobile actions */}
<div className="flex gap-2 pt-4 border-t mt-4">
{onSave && (
<Button
variant="outline"
size="sm"
className="flex-1"
disabled={isSaving || isSubmitting}
onClick={onSave}
>
{isSaving ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
{saveLabel}
</Button>
)}
{onSubmit && (
<Button
size="sm"
className="flex-1"
disabled={isSubmitting || isSaving || !canSubmit}
onClick={onSubmit}
>
{isSubmitting ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Rocket className="h-4 w-4 mr-1" />}
{submitLabel}
</Button>
)}
</div>
</div>
{/* Desktop content */}
<div className="hidden lg:block flex-1 min-w-0">
<StepContent currentStep={currentStep} direction={direction}>
{children}
</StepContent>
</div>
</div>
)
}
function MobileStepIndicator({
steps,
currentStep,
onStepChange,
}: {
steps: StepConfig[]
currentStep: number
onStepChange: (index: number) => void
}) {
return (
<div className="overflow-x-auto">
<div className="flex items-center gap-1 pb-2 min-w-max">
{steps.map((step, index) => {
const isCurrent = index === currentStep
const isComplete = step.isValid === true
return (
<button
key={index}
type="button"
onClick={() => onStepChange(index)}
className={cn(
'flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium transition-colors shrink-0',
isCurrent && 'bg-primary text-primary-foreground',
!isCurrent && isComplete && 'bg-emerald-100 text-emerald-700',
!isCurrent && !isComplete && 'bg-muted text-muted-foreground hover:bg-muted/80'
)}
>
{isComplete && !isCurrent ? (
<CheckCircle2 className="h-3 w-3" />
) : (
<span>{index + 1}</span>
)}
<span className="hidden sm:inline">{step.title}</span>
</button>
)
})}
</div>
</div>
)
}
function StepContent({
currentStep,
direction,
children,
}: {
currentStep: number
direction: (prev: number, next: number) => number
children: React.ReactNode
}) {
const childArray = Array.isArray(children) ? children : [children]
const currentChild = childArray[currentStep]
return (
<div className="relative overflow-hidden">
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.2,
ease: 'easeInOut',
}}
>
{currentChild}
</motion.div>
</AnimatePresence>
</div>
)
}