this is how the edit canva should look
This commit is contained in:
+142
-125
@@ -1,144 +1,161 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useActionState } from 'react';
|
|
||||||
import { useFormStatus } from 'react-dom';
|
|
||||||
import { getFlow, saveFlow, type Flow } from '@/lib/actions/flows';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import {
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
|
Play,
|
||||||
import { useToast } from '@/hooks/use-toast';
|
Share2,
|
||||||
import Link from 'next/link';
|
MessageSquare,
|
||||||
import { ChevronLeft } from 'lucide-react';
|
Pencil,
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
Settings,
|
||||||
|
Bike,
|
||||||
|
Footprints,
|
||||||
|
MoreHorizontal,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Code2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { FlowNode, FlowNodeInput, FlowNodeOption, FlowNodeMessage } from '@/components/admin/flow-node';
|
||||||
|
|
||||||
interface FlowFormPageProps {
|
export default function FlowEditorPage() {
|
||||||
params: { id: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
function SubmitButton({ isNew }: { isNew: boolean }) {
|
|
||||||
const { pending } = useFormStatus();
|
|
||||||
return (
|
return (
|
||||||
<Button type="submit" disabled={pending}>
|
<div className="flex flex-col h-[calc(100vh-4rem)] bg-muted/40">
|
||||||
{pending ? (isNew ? 'Creating...' : 'Saving...') : (isNew ? 'Create Flow' : 'Save Changes')}
|
{/* Header */}
|
||||||
</Button>
|
<header className="flex items-center justify-between p-2 border-b bg-background">
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FlowFormPage({ params }: FlowFormPageProps) {
|
|
||||||
const [state, formAction] = useActionState(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 && !state.success) {
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: state.message,
|
|
||||||
variant: '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>
|
|
||||||
<CardFooter>
|
|
||||||
<Skeleton className="h-10 w-32" />
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getError = (field: string) => {
|
|
||||||
return state.errors?.find(e => e.path?.[0] === field)?.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="outline" size="icon" asChild>
|
<h1 className="text-lg font-semibold">Quick Carb Calculator</h1>
|
||||||
<Link href="/admin/flows">
|
<nav className="flex items-center gap-2">
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<Button variant="ghost" size="sm" className="bg-muted">Flow</Button>
|
||||||
<span className="sr-only">Back</span>
|
<Button variant="ghost" size="sm">Theme</Button>
|
||||||
</Link>
|
<Button variant="ghost" size="sm">Settings</Button>
|
||||||
|
<Button variant="ghost" size="sm">Share</Button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Share2 className="mr-2 h-4 w-4" />
|
||||||
|
Share
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">
|
<Button variant="outline" size="sm">
|
||||||
{isNew ? 'Create New Flow' : 'Edit Flow'}
|
<Play className="mr-2 h-4 w-4" />
|
||||||
</h1>
|
Test
|
||||||
|
</Button>
|
||||||
|
<Button size="sm">Publish</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-2 p-2">
|
||||||
|
{/* Flow Canvas */}
|
||||||
|
<div className="lg:col-span-2 relative bg-dotted-pattern bg-background rounded-lg border overflow-hidden">
|
||||||
|
{/* This is a static representation. A real implementation would use a library like React Flow. */}
|
||||||
|
<div className="absolute top-10 left-10">
|
||||||
|
<FlowNode title="Start" icon={<Play />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action={formAction}>
|
<div className="absolute top-10 left-64 w-80">
|
||||||
<Card>
|
<FlowNode title="Sport">
|
||||||
<CardHeader>
|
<FlowNodeMessage text="Welcome to the Quick Carb Calculator!" />
|
||||||
<CardTitle>Flow Details</CardTitle>
|
<FlowNodeMessage text="I'll help you figure out how much fuel you'll need to perform at your best. Let's get started! ✨" />
|
||||||
<CardDescription>
|
<FlowNodeMessage text="First, what are you fueling for?" />
|
||||||
{isNew ? 'Fill in the details for your new flow.' : 'Update the details for this flow.'}
|
<FlowNodeOption icon={<Bike />} text="Ride" />
|
||||||
</CardDescription>
|
<FlowNodeOption icon={<Footprints />} text="Run" />
|
||||||
</CardHeader>
|
<FlowNodeOption icon={<MoreHorizontal />} text="Other" />
|
||||||
<CardContent className="space-y-6">
|
<div className="p-2 border rounded-md mt-2">
|
||||||
<input type="hidden" name="id" value={flow?.id} />
|
<p className="text-xs text-muted-foreground">Default</p>
|
||||||
<div className="space-y-2">
|
<Badge variant="secondary" className="mt-1">Set Sport</Badge>
|
||||||
<Label htmlFor="name">Flow Name</Label>
|
|
||||||
<Input id="name" name="name" defaultValue={flow?.name} required />
|
|
||||||
{getError('name') && <p className="text-sm text-destructive">{getError('name')}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</FlowNode>
|
||||||
<Label htmlFor="path">Path</Label>
|
|
||||||
<Input id="path" name="path" defaultValue={flow?.path} placeholder="/my-cool-flow" required />
|
|
||||||
{getError('path') && <p className="text-sm text-destructive">{getError('path')}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">Description</Label>
|
<div className="absolute top-10 left-[48rem]">
|
||||||
<Textarea id="description" name="description" defaultValue={flow?.description || ''} placeholder="A brief description of what this flow does." />
|
<FlowNode title="What sport?">
|
||||||
{getError('description') && <p className="text-sm text-destructive">{getError('description')}</p>}
|
<FlowNodeInput placeholder="Type your answer..." variable="Sport" />
|
||||||
|
</FlowNode>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-64 left-[48rem]">
|
||||||
|
<FlowNode title="Duration">
|
||||||
|
<FlowNodeMessage text="Great! How long will you be doing this activity?" />
|
||||||
|
<FlowNodeInput placeholder="Type your answer..." variable="Duration" />
|
||||||
|
</FlowNode>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-64 left-[72rem]">
|
||||||
|
<FlowNode title="Intensity">
|
||||||
|
<FlowNodeMessage text="Understood! Now, how hard will you be going?" />
|
||||||
|
<FlowNodeInput placeholder="Type your answer..." variable="Intensity" />
|
||||||
|
</FlowNode>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-[34rem] left-[72rem]">
|
||||||
|
<FlowNode title="AI gen">
|
||||||
|
<FlowNodeMessage text="Create ..." />
|
||||||
|
<Badge variant="secondary" className="mt-1">Set Assistant</Badge>
|
||||||
|
</FlowNode>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Canvas Controls */}
|
||||||
|
<div className="absolute top-4 right-4 flex flex-col gap-2">
|
||||||
|
<Card className="p-1">
|
||||||
|
<Button variant="ghost" size="icon"><Code2 className="h-4 w-4" /></Button>
|
||||||
|
<Button variant="ghost" size="icon"><MoreHorizontal className="h-4 w-4" /></Button>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-1">
|
||||||
|
<Button variant="ghost" size="icon"><Plus className="h-4 w-4" /></Button>
|
||||||
|
<Button variant="ghost" size="icon"><Minus className="h-4 w-4" /></Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phone Preview */}
|
||||||
|
<div className="lg:col-span-1 bg-background rounded-lg border flex justify-center items-center p-4">
|
||||||
|
<Card className="w-full max-w-sm h-full max-h-[800px] shadow-lg flex flex-col">
|
||||||
|
<CardContent className="p-2 flex-1 overflow-y-auto">
|
||||||
|
<div className="flex flex-col gap-3 p-2">
|
||||||
|
{/* Chat messages */}
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs">
|
||||||
|
First, what are you fueling for?
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs flex items-center gap-2">
|
||||||
|
<Bike className="h-4 w-4" /> Ride
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs flex items-center gap-2">
|
||||||
|
<Footprints className="h-4 w-4" /> Run
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs flex items-center gap-2">
|
||||||
|
<MoreHorizontal className="h-4 w-4" /> Other
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 rounded-lg bg-green-200 text-green-900 self-end max-w-xs flex items-center gap-2">
|
||||||
|
Run <Footprints className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs">
|
||||||
|
Great! How long will you be doing this activity?
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-green-200 text-green-900 self-end max-w-xs">
|
||||||
|
I'll try 3h30
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs">
|
||||||
|
Understood! Now, how hard will you be going?
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-green-200 text-green-900 self-end max-w-xs">
|
||||||
|
I'd say a good 8 out of 10
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg bg-muted text-muted-foreground self-start max-w-xs">
|
||||||
|
Total carbs: 90-120g
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
|
||||||
<SubmitButton isNew={isNew} />
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,9 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
.bg-dotted-pattern {
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
background-image: radial-gradient(hsl(var(--border)) 1px, transparent 0);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { MessageSquare, Pencil, Type } from 'lucide-react';
|
||||||
|
|
||||||
|
interface FlowNodeProps {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlowNode({ title, icon, children, className }: FlowNodeProps) {
|
||||||
|
return (
|
||||||
|
<Card className={`w-72 bg-background shadow-lg ${className}`}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between p-2 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-2 space-y-2">
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlowNodeElementProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlowNodeElement({className, children}: FlowNodeElementProps) {
|
||||||
|
return (
|
||||||
|
<div className={`group relative p-2 text-sm rounded-md border bg-card hover:bg-muted/50 ${className}`}>
|
||||||
|
{children}
|
||||||
|
<Button variant="ghost" size="icon" className="absolute top-1 right-1 h-6 w-6 opacity-0 group-hover:opacity-100">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlowNodeMessage({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<FlowNodeElement>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<MessageSquare className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<p className="flex-1">{text}</p>
|
||||||
|
</div>
|
||||||
|
</FlowNodeElement>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlowNodeInput({ placeholder, variable }: { placeholder: string; variable: string }) {
|
||||||
|
return (
|
||||||
|
<FlowNodeElement>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Type className="h-4 w-4 text-muted-foreground mt-0.5" />
|
||||||
|
<div className='flex-1'>
|
||||||
|
<Input disabled placeholder={placeholder} className="text-xs h-7 bg-transparent disabled:cursor-text disabled:opacity-100" />
|
||||||
|
<div className="mt-2">
|
||||||
|
<Badge variant="secondary">Set {variable}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FlowNodeElement>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlowNodeOption({ text, icon }: { text: string; icon?: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<FlowNodeElement>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon}
|
||||||
|
<p>{text}</p>
|
||||||
|
</div>
|
||||||
|
</FlowNodeElement>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user