on the admin dashboard create a tab called "Flows" and add the estimate

This commit is contained in:
Leon Serfaty G
2025-07-18 04:51:03 +00:00
parent bf1583ba37
commit 6954cf4364
6 changed files with 388 additions and 1 deletions
+134
View File
@@ -0,0 +1,134 @@
'use client';
import { useEffect, useState } from 'react';
import { useFormState } from 'react-dom';
import { getFlow, saveFlow, type Flow } from '@/lib/actions/flows';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast';
import Link from 'next/link';
import { ChevronLeft } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
interface FlowFormPageProps {
params: { id: string };
}
function SubmitButton({ isNew }: { isNew: boolean }) {
return (
<Button type="submit">
{isNew ? 'Create Flow' : 'Save Changes'}
</Button>
);
}
export default function FlowFormPage({ params }: FlowFormPageProps) {
const [state, formAction] = useFormState(saveFlow, { success: false, message: '', errors: null });
const [flow, setFlow] = useState<Flow | null>(null);
const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast();
const isNew = params.id === 'new';
useEffect(() => {
if (!isNew) {
const fetchFlow = async () => {
setIsLoading(true);
const existingFlow = await getFlow(Number(params.id));
setFlow(existingFlow);
setIsLoading(false);
};
fetchFlow();
} else {
setIsLoading(false);
}
}, [params.id, isNew]);
useEffect(() => {
if (state.message) {
toast({
title: state.success ? 'Success!' : 'Error',
description: state.message,
variant: state.success ? 'default' : 'destructive',
});
}
}, [state, toast]);
if (isLoading) {
return (
<div>
<Skeleton className="h-8 w-48 mb-8" />
<Card>
<CardHeader>
<Skeleton className="h-7 w-1/3" />
<Skeleton className="h-5 w-2/3" />
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-5 w-24" />
<Skeleton className="h-20 w-full" />
</div>
</CardContent>
</Card>
<Skeleton className="h-10 w-32 mt-6" />
</div>
)
}
return (
<form action={formAction} className="space-y-8">
<div className="flex items-center gap-4">
<Button variant="outline" size="icon" asChild>
<Link href="/admin/flows">
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Back</span>
</Link>
</Button>
<h1 className="text-3xl font-bold tracking-tight">
{isNew ? 'Create New Flow' : 'Edit Flow'}
</h1>
</div>
<Card>
<CardHeader>
<CardTitle>Flow Details</CardTitle>
<CardDescription>
{isNew ? 'Fill in the details for your new flow.' : 'Update the details for this flow.'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<input type="hidden" name="id" value={flow?.id} />
<div className="space-y-2">
<Label htmlFor="name">Flow Name</Label>
<Input id="name" name="name" defaultValue={flow?.name} required />
{state.errors?.name && <p className="text-sm text-destructive">{state.errors.name[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="path">Path</Label>
<Input id="path" name="path" defaultValue={flow?.path} placeholder="/my-cool-flow" required />
{state.errors?.path && <p className="text-sm text-destructive">{state.errors.path[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea id="description" name="description" defaultValue={flow?.description || ''} placeholder="A brief description of what this flow does." />
{state.errors?.description && <p className="text-sm text-destructive">{state.errors.description[0]}</p>}
</div>
</CardContent>
</Card>
<SubmitButton isNew={isNew} />
</form>
);
}
+107
View File
@@ -0,0 +1,107 @@
import Link from 'next/link';
import { getFlows, type Flow } from '@/lib/actions/flows';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { MoreHorizontal, PlusCircle } from 'lucide-react';
import { format } from 'date-fns';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from '@/components/ui/badge';
export default async function FlowsPage() {
const flows = await getFlows();
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Flows</h1>
<p className="mt-2 text-muted-foreground">
Manage your interactive application flows.
</p>
</div>
<Link href="/admin/flows/new">
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
Add New Flow
</Button>
</Link>
</div>
<Card>
<CardHeader>
<CardTitle>All Flows</CardTitle>
<CardDescription>
A list of all flows in your application.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Path</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{flows.length > 0 ? (
flows.map((flow) => (
<TableRow key={flow.id}>
<TableCell className="font-medium">{flow.name}</TableCell>
<TableCell>{flow.description || 'N/A'}</TableCell>
<TableCell>
<Badge variant="outline">{flow.path}</Badge>
</TableCell>
<TableCell>
{format(new Date(flow.createdAt), 'PPP')}
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<Link href={`/admin/flows/${flow.id}`}>
<DropdownMenuItem>Edit</DropdownMenuItem>
</Link>
<Link href={`${flow.path}`} target="_blank">
<DropdownMenuItem>View</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:bg-destructive/10 focus:text-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center h-24">
No flows found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}
+10 -1
View File
@@ -25,7 +25,8 @@ import {
Mails,
Send,
ClipboardList,
ExternalLink
ExternalLink,
Workflow
} from "lucide-react"
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -61,6 +62,14 @@ function AdminLayout({
<span>Leads</span>
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/flows">
<SidebarMenuButton tooltip="Flows">
<Workflow />
<span>Flows</span>
</SidebarMenuButton>
</Link>
</SidebarMenuItem>
<SidebarMenuItem>
<Link href="/admin/embed">