@nebulit/embuilder 0.1.43 → 0.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -2
- package/package.json +1 -1
- package/templates/backend/prompt.md +6 -6
- package/templates/frontend/prompt.md +24 -0
- package/templates/frontend/src/lib/api-client.ts +60 -98
- package/templates/frontend/src/lib/api.ts +28 -1028
- package/templates/frontend/src/pages/Register.tsx +3 -3
- package/templates/prompt.md +7 -6
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +0 -189
- package/templates/frontend/src/pages/Menus.tsx +0 -224
|
@@ -8,7 +8,7 @@ import { useToast } from '@/hooks/use-toast';
|
|
|
8
8
|
import { Check, ChevronLeft } from 'lucide-react';
|
|
9
9
|
import { supabase } from '@/integrations/supabase/client';
|
|
10
10
|
import { useAuth } from '@/contexts/AuthContext';
|
|
11
|
-
import { registerTenant } from "@/lib/api";
|
|
11
|
+
//import { registerTenant } from "@/lib/api";
|
|
12
12
|
import { v4 } from "uuid";
|
|
13
13
|
|
|
14
14
|
const Register = () => {
|
|
@@ -113,11 +113,11 @@ const Register = () => {
|
|
|
113
113
|
const token = sessionData?.session?.access_token ?? "";
|
|
114
114
|
const tenantId = v4();
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
/* await registerTenant({
|
|
117
117
|
tenantId,
|
|
118
118
|
name: tenantName,
|
|
119
119
|
ownerId: ownerId,
|
|
120
|
-
}, { token, userId: ownerId, tenantId })
|
|
120
|
+
}, { token, userId: ownerId, tenantId });*/
|
|
121
121
|
|
|
122
122
|
toast({
|
|
123
123
|
title: 'Success',
|
package/templates/prompt.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Ralph Agent Instructions
|
|
2
2
|
|
|
3
|
-
You are an autonomous software architect,
|
|
4
|
-
|
|
5
|
-
The structure defined in the Project-Skills is relevant.
|
|
3
|
+
You are an autonomous software architect, overseeing the sliced architecture and who works on which slice.
|
|
4
|
+
You assign tasks to workers. Only one task can be assigned and in status planned at a time.
|
|
6
5
|
|
|
7
6
|
## Your Task
|
|
8
7
|
|
|
9
|
-
1. find the most important next slice
|
|
10
|
-
2. if the slice is in status planned and not assigned, assign it to backend_worker (property assigned
|
|
11
|
-
3. if the
|
|
8
|
+
1. find the most important next slice by watching .slices/index.json
|
|
9
|
+
2. if the slice is in status planned and not assigned, assign it to backend_worker (property "assigned" ) and continue with backend/prompt.md. Ignore the rest of this file.
|
|
10
|
+
3. if the status is in status "InProgress" and assigned to backend_worker, updated started_time and continue with backend/prompt.md. Ignore the rest of this file.
|
|
11
|
+
4. if the status is "done" and assigned to "backend_worker", assign the task to "ui_worker" and move it back to status "planned". continue with frontend/prompt.md. Ignore the rest of this file.
|
|
12
|
+
4. if there is no task in status planned, return <promise>NO_TASKS</promise>
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { useRef, useState, useEffect } from "react";
|
|
2
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
5
|
-
import { Label } from "@/components/ui/label";
|
|
6
|
-
import { Loader2, Mail, Phone } from "lucide-react";
|
|
7
|
-
import { toast } from "sonner";
|
|
8
|
-
import { useReservationTemplates, useSaveReservationTemplate } from "@/hooks/api/useReservationTemplates";
|
|
9
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
|
-
|
|
11
|
-
const TEMPLATE_VARIABLES = [
|
|
12
|
-
{ key: "name", label: "Name" },
|
|
13
|
-
{ key: "from", label: "From" },
|
|
14
|
-
{ key: "to", label: "To" },
|
|
15
|
-
{ key: "persons", label: "Persons" },
|
|
16
|
-
{ key: "location", label: "Location" },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
export function ReservationTemplates() {
|
|
20
|
-
const { data: templates = [], isLoading } = useReservationTemplates();
|
|
21
|
-
const saveMutation = useSaveReservationTemplate();
|
|
22
|
-
|
|
23
|
-
const [emailTemplate, setEmailTemplate] = useState("");
|
|
24
|
-
const [phoneTemplate, setPhoneTemplate] = useState("");
|
|
25
|
-
const [focusedField, setFocusedField] = useState<"email" | "phone" | null>(null);
|
|
26
|
-
|
|
27
|
-
const emailRef = useRef<HTMLTextAreaElement>(null);
|
|
28
|
-
const phoneRef = useRef<HTMLTextAreaElement>(null);
|
|
29
|
-
|
|
30
|
-
// Load templates when data is fetched
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
const emailTpl = templates.find(t => t.templateType === "EMAIL");
|
|
33
|
-
const phoneTpl = templates.find(t => t.templateType === "PHONE");
|
|
34
|
-
|
|
35
|
-
if (emailTpl) setEmailTemplate(emailTpl.template);
|
|
36
|
-
if (phoneTpl) setPhoneTemplate(phoneTpl.template);
|
|
37
|
-
}, [templates]);
|
|
38
|
-
|
|
39
|
-
const insertVariable = (variable: string) => {
|
|
40
|
-
const insertion = `{${variable}}`;
|
|
41
|
-
|
|
42
|
-
if (focusedField === "email" && emailRef.current) {
|
|
43
|
-
const textarea = emailRef.current;
|
|
44
|
-
const start = textarea.selectionStart;
|
|
45
|
-
const end = textarea.selectionEnd;
|
|
46
|
-
const newValue = emailTemplate.slice(0, start) + insertion + emailTemplate.slice(end);
|
|
47
|
-
setEmailTemplate(newValue);
|
|
48
|
-
|
|
49
|
-
// Restore cursor position after insertion
|
|
50
|
-
setTimeout(() => {
|
|
51
|
-
textarea.focus();
|
|
52
|
-
textarea.setSelectionRange(start + insertion.length, start + insertion.length);
|
|
53
|
-
}, 0);
|
|
54
|
-
} else if (focusedField === "phone" && phoneRef.current) {
|
|
55
|
-
const textarea = phoneRef.current;
|
|
56
|
-
const start = textarea.selectionStart;
|
|
57
|
-
const end = textarea.selectionEnd;
|
|
58
|
-
const newValue = phoneTemplate.slice(0, start) + insertion + phoneTemplate.slice(end);
|
|
59
|
-
setPhoneTemplate(newValue);
|
|
60
|
-
|
|
61
|
-
setTimeout(() => {
|
|
62
|
-
textarea.focus();
|
|
63
|
-
textarea.setSelectionRange(start + insertion.length, start + insertion.length);
|
|
64
|
-
}, 0);
|
|
65
|
-
} else {
|
|
66
|
-
toast.error("Please select a text field first");
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const handleSaveEmail = async () => {
|
|
71
|
-
const existingTemplate = templates.find(t => t.templateType === "EMAIL");
|
|
72
|
-
const templateId = existingTemplate?.templateId || `tpl-email-${Date.now()}`;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await saveMutation.mutateAsync({
|
|
76
|
-
templateId,
|
|
77
|
-
templateType: "EMAIL",
|
|
78
|
-
template: emailTemplate,
|
|
79
|
-
});
|
|
80
|
-
toast.success("Email template saved");
|
|
81
|
-
} catch (err) {
|
|
82
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleSavePhone = async () => {
|
|
87
|
-
const existingTemplate = templates.find(t => t.templateType === "PHONE");
|
|
88
|
-
const templateId = existingTemplate?.templateId || `tpl-phone-${Date.now()}`;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await saveMutation.mutateAsync({
|
|
92
|
-
templateId,
|
|
93
|
-
templateType: "PHONE",
|
|
94
|
-
template: phoneTemplate,
|
|
95
|
-
});
|
|
96
|
-
toast.success("SMS template saved");
|
|
97
|
-
} catch (err) {
|
|
98
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
if (isLoading) {
|
|
103
|
-
return (
|
|
104
|
-
<Card>
|
|
105
|
-
<CardHeader>
|
|
106
|
-
<Skeleton className="h-6 w-48" />
|
|
107
|
-
</CardHeader>
|
|
108
|
-
<CardContent className="space-y-4">
|
|
109
|
-
<Skeleton className="h-32 w-full" />
|
|
110
|
-
</CardContent>
|
|
111
|
-
</Card>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Card>
|
|
117
|
-
<CardHeader>
|
|
118
|
-
<CardTitle>Notification Templates</CardTitle>
|
|
119
|
-
</CardHeader>
|
|
120
|
-
<CardContent className="space-y-6">
|
|
121
|
-
{/* Variable buttons */}
|
|
122
|
-
<div className="flex flex-wrap gap-2">
|
|
123
|
-
{TEMPLATE_VARIABLES.map((variable) => (
|
|
124
|
-
<Button
|
|
125
|
-
key={variable.key}
|
|
126
|
-
variant="outline"
|
|
127
|
-
size="sm"
|
|
128
|
-
onClick={() => insertVariable(variable.key)}
|
|
129
|
-
className="text-xs"
|
|
130
|
-
>
|
|
131
|
-
{variable.label}
|
|
132
|
-
</Button>
|
|
133
|
-
))}
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{/* Templates grid */}
|
|
137
|
-
<div className="grid gap-6 md:grid-cols-2">
|
|
138
|
-
{/* Email template */}
|
|
139
|
-
<div className="space-y-3">
|
|
140
|
-
<div className="flex items-center gap-2">
|
|
141
|
-
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
142
|
-
<Label>Email</Label>
|
|
143
|
-
</div>
|
|
144
|
-
<Textarea
|
|
145
|
-
ref={emailRef}
|
|
146
|
-
value={emailTemplate}
|
|
147
|
-
onChange={(e) => setEmailTemplate(e.target.value)}
|
|
148
|
-
onFocus={() => setFocusedField("email")}
|
|
149
|
-
placeholder="Enter email template..."
|
|
150
|
-
className="min-h-[150px] resize-none"
|
|
151
|
-
/>
|
|
152
|
-
<Button
|
|
153
|
-
onClick={handleSaveEmail}
|
|
154
|
-
disabled={saveMutation.isPending}
|
|
155
|
-
className="w-full"
|
|
156
|
-
>
|
|
157
|
-
{saveMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
158
|
-
Save
|
|
159
|
-
</Button>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
{/* Phone/SMS template */}
|
|
163
|
-
<div className="space-y-3">
|
|
164
|
-
<div className="flex items-center gap-2">
|
|
165
|
-
<Phone className="h-4 w-4 text-muted-foreground" />
|
|
166
|
-
<Label>SMS</Label>
|
|
167
|
-
</div>
|
|
168
|
-
<Textarea
|
|
169
|
-
ref={phoneRef}
|
|
170
|
-
value={phoneTemplate}
|
|
171
|
-
onChange={(e) => setPhoneTemplate(e.target.value)}
|
|
172
|
-
onFocus={() => setFocusedField("phone")}
|
|
173
|
-
placeholder="Enter SMS template..."
|
|
174
|
-
className="min-h-[150px] resize-none"
|
|
175
|
-
/>
|
|
176
|
-
<Button
|
|
177
|
-
onClick={handleSavePhone}
|
|
178
|
-
disabled={saveMutation.isPending}
|
|
179
|
-
className="w-full"
|
|
180
|
-
>
|
|
181
|
-
{saveMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
182
|
-
Save
|
|
183
|
-
</Button>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
</CardContent>
|
|
187
|
-
</Card>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { DashboardLayout } from "@/components/layout/DashboardLayout";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
5
|
-
import { Label } from "@/components/ui/label";
|
|
6
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
7
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
8
|
-
import { Card, CardContent } from "@/components/ui/card";
|
|
9
|
-
import { Plus, Trash2, Download, FileText, Loader2 } from "lucide-react";
|
|
10
|
-
import { Menu } from "@/types";
|
|
11
|
-
import { Badge } from "@/components/ui/badge";
|
|
12
|
-
import { toast } from "sonner";
|
|
13
|
-
import { useMenus, useUploadMenu, useDeleteMenu } from "@/hooks/api";
|
|
14
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
15
|
-
|
|
16
|
-
const documentTypes = [
|
|
17
|
-
{ value: "primary", label: "Primary" },
|
|
18
|
-
{ value: "secondary", label: "Secondary" },
|
|
19
|
-
{ value: "report", label: "Report" },
|
|
20
|
-
{ value: "template", label: "Template" },
|
|
21
|
-
{ value: "archive", label: "Archive" },
|
|
22
|
-
{ value: "other", label: "Other" },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
export default function Menus() {
|
|
26
|
-
const { data: menus = [], isLoading, error } = useMenus();
|
|
27
|
-
const uploadMenuMutation = useUploadMenu();
|
|
28
|
-
const deleteMenuMutation = useDeleteMenu();
|
|
29
|
-
|
|
30
|
-
const [isAddOpen, setIsAddOpen] = useState(false);
|
|
31
|
-
const [formData, setFormData] = useState({
|
|
32
|
-
menuName: "",
|
|
33
|
-
menuType: "",
|
|
34
|
-
file: "",
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const handleAddMenu = async () => {
|
|
38
|
-
if (!formData.menuName || !formData.menuType) {
|
|
39
|
-
toast.error("Please fill in all required fields");
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await uploadMenuMutation.mutateAsync({
|
|
45
|
-
menuName: formData.menuName,
|
|
46
|
-
menuType: formData.menuType,
|
|
47
|
-
file: formData.file,
|
|
48
|
-
restaurantId: "", // Will be set from context headers
|
|
49
|
-
});
|
|
50
|
-
setFormData({ menuName: "", menuType: "", file: "" });
|
|
51
|
-
setIsAddOpen(false);
|
|
52
|
-
toast.success("Document uploaded successfully");
|
|
53
|
-
} catch (err) {
|
|
54
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const deleteMenu = async (menu: Menu) => {
|
|
59
|
-
try {
|
|
60
|
-
await deleteMenuMutation.mutateAsync(menu.menuId);
|
|
61
|
-
toast.success(`"${menu.menuName}" deleted`);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const getTypeColor = (type: string) => {
|
|
68
|
-
const colors: Record<string, string> = {
|
|
69
|
-
primary: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300",
|
|
70
|
-
secondary: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300",
|
|
71
|
-
report: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",
|
|
72
|
-
template: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",
|
|
73
|
-
archive: "bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300",
|
|
74
|
-
other: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300",
|
|
75
|
-
};
|
|
76
|
-
return colors[type] || "bg-muted text-muted-foreground";
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const getTypeLabel = (type: string) => {
|
|
80
|
-
const docType = documentTypes.find((t) => t.value === type);
|
|
81
|
-
return docType?.label || type;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
if (error) {
|
|
85
|
-
return (
|
|
86
|
-
<DashboardLayout title="Documents" subtitle="Manage your documents">
|
|
87
|
-
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
88
|
-
<p className="text-destructive">Error loading: {error.message}</p>
|
|
89
|
-
<p className="mt-2 text-sm text-muted-foreground">
|
|
90
|
-
Check the API settings (Settings icon in the header)
|
|
91
|
-
</p>
|
|
92
|
-
</div>
|
|
93
|
-
</DashboardLayout>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<DashboardLayout title="Documents" subtitle="Manage your documents">
|
|
99
|
-
<div className="mb-6 flex items-center justify-between">
|
|
100
|
-
<p className="text-muted-foreground">
|
|
101
|
-
{isLoading ? "Loading..." : `${menus.length} document${menus.length !== 1 ? "s" : ""} uploaded`}
|
|
102
|
-
</p>
|
|
103
|
-
<Dialog open={isAddOpen} onOpenChange={setIsAddOpen}>
|
|
104
|
-
<DialogTrigger asChild>
|
|
105
|
-
<Button>
|
|
106
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
107
|
-
Upload document
|
|
108
|
-
</Button>
|
|
109
|
-
</DialogTrigger>
|
|
110
|
-
<DialogContent>
|
|
111
|
-
<DialogHeader>
|
|
112
|
-
<DialogTitle>Upload new document</DialogTitle>
|
|
113
|
-
</DialogHeader>
|
|
114
|
-
<div className="space-y-4 pt-4">
|
|
115
|
-
<div>
|
|
116
|
-
<Label htmlFor="menuName">Document name *</Label>
|
|
117
|
-
<Input
|
|
118
|
-
id="menuName"
|
|
119
|
-
value={formData.menuName}
|
|
120
|
-
onChange={(e) => setFormData({ ...formData, menuName: e.target.value })}
|
|
121
|
-
placeholder="e.g. Q1 Report"
|
|
122
|
-
/>
|
|
123
|
-
</div>
|
|
124
|
-
<div>
|
|
125
|
-
<Label htmlFor="menuType">Document type *</Label>
|
|
126
|
-
<Select
|
|
127
|
-
value={formData.menuType}
|
|
128
|
-
onValueChange={(value) => setFormData({ ...formData, menuType: value })}
|
|
129
|
-
>
|
|
130
|
-
<SelectTrigger>
|
|
131
|
-
<SelectValue placeholder="Select type" />
|
|
132
|
-
</SelectTrigger>
|
|
133
|
-
<SelectContent>
|
|
134
|
-
{documentTypes.map((type) => (
|
|
135
|
-
<SelectItem key={type.value} value={type.value}>
|
|
136
|
-
{type.label}
|
|
137
|
-
</SelectItem>
|
|
138
|
-
))}
|
|
139
|
-
</SelectContent>
|
|
140
|
-
</Select>
|
|
141
|
-
</div>
|
|
142
|
-
<div>
|
|
143
|
-
<Label htmlFor="file">File</Label>
|
|
144
|
-
<Input
|
|
145
|
-
id="file"
|
|
146
|
-
type="file"
|
|
147
|
-
accept=".pdf,.doc,.docx"
|
|
148
|
-
onChange={(e) => setFormData({ ...formData, file: e.target.files?.[0]?.name || "" })}
|
|
149
|
-
/>
|
|
150
|
-
<p className="mt-1 text-xs text-muted-foreground">PDF or Word documents are accepted</p>
|
|
151
|
-
</div>
|
|
152
|
-
<Button
|
|
153
|
-
onClick={handleAddMenu}
|
|
154
|
-
className="w-full"
|
|
155
|
-
disabled={uploadMenuMutation.isPending}
|
|
156
|
-
>
|
|
157
|
-
{uploadMenuMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
158
|
-
Upload document
|
|
159
|
-
</Button>
|
|
160
|
-
</div>
|
|
161
|
-
</DialogContent>
|
|
162
|
-
</Dialog>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{isLoading ? (
|
|
166
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
167
|
-
{[1, 2, 3].map((i) => (
|
|
168
|
-
<Card key={i}>
|
|
169
|
-
<CardContent className="p-4">
|
|
170
|
-
<div className="flex items-center gap-3">
|
|
171
|
-
<Skeleton className="h-12 w-12 rounded-lg" />
|
|
172
|
-
<div>
|
|
173
|
-
<Skeleton className="h-4 w-32 mb-2" />
|
|
174
|
-
<Skeleton className="h-3 w-24" />
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</CardContent>
|
|
178
|
-
</Card>
|
|
179
|
-
))}
|
|
180
|
-
</div>
|
|
181
|
-
) : (
|
|
182
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
183
|
-
{menus.map((menu) => (
|
|
184
|
-
<Card key={menu.menuId} className="group transition-shadow hover:shadow-md animate-fade-in">
|
|
185
|
-
<CardContent className="p-4">
|
|
186
|
-
<div className="flex items-start justify-between">
|
|
187
|
-
<div className="flex items-center gap-3">
|
|
188
|
-
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
|
189
|
-
<FileText className="h-6 w-6 text-primary" />
|
|
190
|
-
</div>
|
|
191
|
-
<div>
|
|
192
|
-
<h3 className="font-medium text-foreground">{menu.menuName}</h3>
|
|
193
|
-
<p className="text-sm text-muted-foreground">{menu.file || "No file"}</p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div className="mt-4 flex items-center justify-between">
|
|
199
|
-
<Badge className={getTypeColor(menu.menuType)}>
|
|
200
|
-
{getTypeLabel(menu.menuType)}
|
|
201
|
-
</Badge>
|
|
202
|
-
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
|
203
|
-
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
204
|
-
<Download className="h-4 w-4" />
|
|
205
|
-
</Button>
|
|
206
|
-
<Button
|
|
207
|
-
variant="ghost"
|
|
208
|
-
size="icon"
|
|
209
|
-
className="h-8 w-8 text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
|
210
|
-
onClick={() => deleteMenu(menu)}
|
|
211
|
-
disabled={deleteMenuMutation.isPending}
|
|
212
|
-
>
|
|
213
|
-
<Trash2 className="h-4 w-4" />
|
|
214
|
-
</Button>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
</CardContent>
|
|
218
|
-
</Card>
|
|
219
|
-
))}
|
|
220
|
-
</div>
|
|
221
|
-
)}
|
|
222
|
-
</DashboardLayout>
|
|
223
|
-
);
|
|
224
|
-
}
|