@nebulit/embuilder 0.1.39
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/README.md +254 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/package.json +49 -0
- package/templates/.claude/hooks/QUICKSTART.md +256 -0
- package/templates/.claude/hooks/README.md +533 -0
- package/templates/.claude/hooks/analyze-commit.sh +22 -0
- package/templates/.claude/hooks/analyze-commit.ts +518 -0
- package/templates/.claude/hooks/analyzers/README.md +198 -0
- package/templates/.claude/hooks/analyzers/code-quality-checker.ts +154 -0
- package/templates/.claude/hooks/analyzers/code-quality.md +54 -0
- package/templates/.claude/hooks/analyzers/commit-blocker-example.ts.disabled +110 -0
- package/templates/.claude/hooks/analyzers/commit-policy.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.md +49 -0
- package/templates/.claude/hooks/analyzers/event-model-validator.ts +169 -0
- package/templates/.claude/hooks/analyzers/example-logger.ts +70 -0
- package/templates/.claude/hooks/analyzers/slice-scope-validator.md +81 -0
- package/templates/.claude/hooks/check-review-result.sh +47 -0
- package/templates/.claude/hooks/prepare-review.sh +34 -0
- package/templates/.claude/hooks/review-agent-prompt.md +42 -0
- package/templates/.claude/hooks/run-review-agent.sh +124 -0
- package/templates/.claude/settings.local.json +37 -0
- package/templates/.claude/skills/help/README.md +84 -0
- package/templates/.claude/skills/help/SKILL.md +393 -0
- package/templates/.claude/skills/help/templates/demo-config.json +6753 -0
- package/templates/.claude/skills/sample-slices/SKILL.md +8 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +124 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/slice.json +255 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/availablebooks/slice.json +107 -0
- package/templates/.claude/skills/sample-slices/templates/.slices/index.json +20 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/additem/slice.json +979 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/archiveitem/slice.json +529 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartitems/slice.json +1072 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/cartwithproducts/slice.json +394 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changedprices/slice.json +88 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeinventory/slice.json +264 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/changeprice/slice.json +308 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/clearcart/slice.json +358 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/inventories/slice.json +203 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/publishcart/slice.json +876 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/removeitem/slice.json +560 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submitcart/slice.json +708 -0
- package/templates/.claude/skills/sample-slices/templates/Cart/submittedcartdata/slice.json +399 -0
- package/templates/.claude/skills/sample-slices/templates/index.json +108 -0
- package/templates/.claude/skills/slice-automation/SKILL.md +49 -0
- package/templates/.claude/skills/slice-state-change/SKILL.md +369 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocation.test.ts.sample +76 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/AddLocationCommand.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-change/templates/AddLocation/routes.ts.sample +73 -0
- package/templates/.claude/skills/slice-state-change/templates/README.md +46 -0
- package/templates/.claude/skills/slice-state-view/SKILL.md +336 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/Locations.test.ts.sample +84 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/LocationsProjection.ts.sample +50 -0
- package/templates/.claude/skills/slice-state-view/templates/Locations/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/README.md +109 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/Tables.test.ts.sample +104 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/TablesProjection.ts.sample +59 -0
- package/templates/.claude/skills/slice-state-view/templates/Tables/routes.ts.sample +46 -0
- package/templates/.claude/skills/slice-state-view/templates/V2__tables.sql +7 -0
- package/templates/.claude/skills/slice-state-view/templates/V8__locations.sql +7 -0
- package/templates/.claude/skills/test-analyzer/SKILL.md +373 -0
- package/templates/.claude/skills/test-analyzer/examples/specification-format.md +143 -0
- package/templates/.claude/skills/test-analyzer/examples/state-change-example.md +111 -0
- package/templates/.claude/skills/test-analyzer/examples/state-view-example.md +122 -0
- package/templates/AGENTS.md +110 -0
- package/templates/Claude.md +58 -0
- package/templates/README.md +178 -0
- package/templates/backend/.env +9 -0
- package/templates/backend/BACKEND_AUTH_SETUP.md +183 -0
- package/templates/backend/SWAGGER.md +213 -0
- package/templates/backend/eslint.config.mjs +31 -0
- package/templates/backend/flyway.conf +17 -0
- package/templates/backend/package.json +44 -0
- package/templates/backend/prd.json.example +64 -0
- package/templates/backend/public/assets/images/banner.png +0 -0
- package/templates/backend/public/assets/logo.png +0 -0
- package/templates/backend/public/file.svg +4 -0
- package/templates/backend/public/globe.svg +12 -0
- package/templates/backend/public/next.svg +6 -0
- package/templates/backend/public/vercel.svg +3 -0
- package/templates/backend/public/window.svg +5 -0
- package/templates/backend/server.ts +129 -0
- package/templates/backend/setup-env.sh +50 -0
- package/templates/backend/src/common/assertions.ts +6 -0
- package/templates/backend/src/common/db.ts +1 -0
- package/templates/backend/src/common/loadPostgresEventstore.ts +16 -0
- package/templates/backend/src/common/parseEndpoint.ts +51 -0
- package/templates/backend/src/common/replay.ts +9 -0
- package/templates/backend/src/common/routes.ts +19 -0
- package/templates/backend/src/common/testHelpers.ts +53 -0
- package/templates/backend/src/core/readmodel.ts +28 -0
- package/templates/backend/src/core/types.ts +26 -0
- package/templates/backend/src/process/process.ts +53 -0
- package/templates/backend/src/supabase/LoginHandler.ts +36 -0
- package/templates/backend/src/supabase/ProtectedPageProps.ts +21 -0
- package/templates/backend/src/supabase/README.md +171 -0
- package/templates/backend/src/supabase/api.ts +63 -0
- package/templates/backend/src/supabase/authMiddleware.ts +53 -0
- package/templates/backend/src/supabase/component.ts +12 -0
- package/templates/backend/src/supabase/requireUser.ts +72 -0
- package/templates/backend/src/supabase/serverProps.ts +25 -0
- package/templates/backend/src/supabase/staticProps.ts +10 -0
- package/templates/backend/src/swagger.ts +34 -0
- package/templates/backend/src/util/assertions.ts +6 -0
- package/templates/backend/supabase/config.toml +295 -0
- package/templates/backend/supabase/migrations/20260121155918593_catalogentries.sql.sample +23 -0
- package/templates/backend/supabase/seed.sql +1 -0
- package/templates/backend/tsconfig.json +31 -0
- package/templates/frontend/.env.development +3 -0
- package/templates/frontend/AGENTS.md +7 -0
- package/templates/frontend/README.md +73 -0
- package/templates/frontend/components.json +20 -0
- package/templates/frontend/eslint.config.js +26 -0
- package/templates/frontend/index.html +18 -0
- package/templates/frontend/package-lock.json +8347 -0
- package/templates/frontend/package.json +94 -0
- package/templates/frontend/postcss.config.js +6 -0
- package/templates/frontend/public/favicon.ico +0 -0
- package/templates/frontend/public/logo.png +0 -0
- package/templates/frontend/public/placeholder.svg +1 -0
- package/templates/frontend/public/robots.txt +14 -0
- package/templates/frontend/src/App.css +42 -0
- package/templates/frontend/src/App.tsx +47 -0
- package/templates/frontend/src/components/NavLink.tsx +28 -0
- package/templates/frontend/src/components/ProtectedRoute.tsx +24 -0
- package/templates/frontend/src/components/calendar/Calendar.tsx +302 -0
- package/templates/frontend/src/components/layout/DashboardLayout.tsx +21 -0
- package/templates/frontend/src/components/layout/Header.tsx +45 -0
- package/templates/frontend/src/components/layout/Sidebar.tsx +82 -0
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +189 -0
- package/templates/frontend/src/components/ui/accordion.tsx +52 -0
- package/templates/frontend/src/components/ui/alert-dialog.tsx +104 -0
- package/templates/frontend/src/components/ui/alert.tsx +43 -0
- package/templates/frontend/src/components/ui/aspect-ratio.tsx +5 -0
- package/templates/frontend/src/components/ui/avatar.tsx +38 -0
- package/templates/frontend/src/components/ui/badge.tsx +29 -0
- package/templates/frontend/src/components/ui/breadcrumb.tsx +90 -0
- package/templates/frontend/src/components/ui/button.tsx +47 -0
- package/templates/frontend/src/components/ui/calendar.tsx +54 -0
- package/templates/frontend/src/components/ui/card.tsx +43 -0
- package/templates/frontend/src/components/ui/carousel.tsx +224 -0
- package/templates/frontend/src/components/ui/chart.tsx +303 -0
- package/templates/frontend/src/components/ui/checkbox.tsx +26 -0
- package/templates/frontend/src/components/ui/collapsible.tsx +9 -0
- package/templates/frontend/src/components/ui/command.tsx +132 -0
- package/templates/frontend/src/components/ui/context-menu.tsx +178 -0
- package/templates/frontend/src/components/ui/dialog.tsx +95 -0
- package/templates/frontend/src/components/ui/drawer.tsx +87 -0
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +179 -0
- package/templates/frontend/src/components/ui/form.tsx +129 -0
- package/templates/frontend/src/components/ui/hover-card.tsx +27 -0
- package/templates/frontend/src/components/ui/input-otp.tsx +61 -0
- package/templates/frontend/src/components/ui/input.tsx +22 -0
- package/templates/frontend/src/components/ui/label.tsx +17 -0
- package/templates/frontend/src/components/ui/menubar.tsx +207 -0
- package/templates/frontend/src/components/ui/navigation-menu.tsx +120 -0
- package/templates/frontend/src/components/ui/pagination.tsx +81 -0
- package/templates/frontend/src/components/ui/popover.tsx +29 -0
- package/templates/frontend/src/components/ui/progress.tsx +23 -0
- package/templates/frontend/src/components/ui/radio-group.tsx +36 -0
- package/templates/frontend/src/components/ui/resizable.tsx +37 -0
- package/templates/frontend/src/components/ui/scroll-area.tsx +38 -0
- package/templates/frontend/src/components/ui/select.tsx +143 -0
- package/templates/frontend/src/components/ui/separator.tsx +20 -0
- package/templates/frontend/src/components/ui/sheet.tsx +107 -0
- package/templates/frontend/src/components/ui/sidebar.tsx +637 -0
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -0
- package/templates/frontend/src/components/ui/slider.tsx +23 -0
- package/templates/frontend/src/components/ui/sonner.tsx +27 -0
- package/templates/frontend/src/components/ui/stat-card.tsx +44 -0
- package/templates/frontend/src/components/ui/switch.tsx +27 -0
- package/templates/frontend/src/components/ui/table.tsx +72 -0
- package/templates/frontend/src/components/ui/tabs.tsx +53 -0
- package/templates/frontend/src/components/ui/textarea.tsx +21 -0
- package/templates/frontend/src/components/ui/toast.tsx +111 -0
- package/templates/frontend/src/components/ui/toaster.tsx +24 -0
- package/templates/frontend/src/components/ui/toggle-group.tsx +49 -0
- package/templates/frontend/src/components/ui/toggle.tsx +37 -0
- package/templates/frontend/src/components/ui/tooltip.tsx +28 -0
- package/templates/frontend/src/components/ui/use-toast.ts +3 -0
- package/templates/frontend/src/contexts/AuthContext.tsx +94 -0
- package/templates/frontend/src/contexts/RefreshContext.tsx +236 -0
- package/templates/frontend/src/hooks/api/index.ts +2 -0
- package/templates/frontend/src/hooks/api/useLocations.ts +15 -0
- package/templates/frontend/src/hooks/use-mobile.tsx +19 -0
- package/templates/frontend/src/hooks/use-toast.ts +186 -0
- package/templates/frontend/src/hooks/useApiContext.ts +11 -0
- package/templates/frontend/src/index.css +118 -0
- package/templates/frontend/src/integrations/supabase/client.ts +9 -0
- package/templates/frontend/src/lib/api-client.ts +136 -0
- package/templates/frontend/src/lib/api.ts +1028 -0
- package/templates/frontend/src/lib/utils.ts +6 -0
- package/templates/frontend/src/main.tsx +5 -0
- package/templates/frontend/src/pages/Auth.tsx +408 -0
- package/templates/frontend/src/pages/Dashboard.tsx +168 -0
- package/templates/frontend/src/pages/Menus.tsx +224 -0
- package/templates/frontend/src/pages/NotFound.tsx +24 -0
- package/templates/frontend/src/pages/Register.tsx +285 -0
- package/templates/frontend/src/test/example.test.ts +0 -0
- package/templates/frontend/src/test/setup.ts +15 -0
- package/templates/frontend/src/types/index.ts +8 -0
- package/templates/frontend/src/vite-env.d.ts +1 -0
- package/templates/frontend/tailwind.config.ts +101 -0
- package/templates/frontend/tsconfig.app.json +31 -0
- package/templates/frontend/tsconfig.json +16 -0
- package/templates/frontend/tsconfig.node.json +22 -0
- package/templates/frontend/vite.config.ts +21 -0
- package/templates/frontend/vitest.config.ts +16 -0
- package/templates/init.sh +1 -0
- package/templates/prompt.md +139 -0
- package/templates/ralph.sh +120 -0
- package/templates/server.mjs +505 -0
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useLocation } from "react-router-dom";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
const NotFound = () => {
|
|
5
|
+
const location = useLocation();
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
|
|
9
|
+
}, [location.pathname]);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-screen items-center justify-center bg-muted">
|
|
13
|
+
<div className="text-center">
|
|
14
|
+
<h1 className="mb-4 text-4xl font-bold">404</h1>
|
|
15
|
+
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
|
|
16
|
+
<a href="/" className="text-primary underline hover:text-primary/90">
|
|
17
|
+
Return to Home
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default NotFound;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Input } from '@/components/ui/input';
|
|
5
|
+
import { Label } from '@/components/ui/label';
|
|
6
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
7
|
+
import { useToast } from '@/hooks/use-toast';
|
|
8
|
+
import { Check, ChevronLeft } from 'lucide-react';
|
|
9
|
+
import { supabase } from '@/integrations/supabase/client';
|
|
10
|
+
import { useAuth } from '@/contexts/AuthContext';
|
|
11
|
+
import { registerTenant } from "@/lib/api";
|
|
12
|
+
import { v4 } from "uuid";
|
|
13
|
+
|
|
14
|
+
const Register = () => {
|
|
15
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
16
|
+
const [email, setEmail] = useState('');
|
|
17
|
+
const [password, setPassword] = useState('');
|
|
18
|
+
const [tenantName, setTenantName] = useState('');
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const { user } = useAuth();
|
|
21
|
+
const navigate = useNavigate();
|
|
22
|
+
const { toast } = useToast();
|
|
23
|
+
|
|
24
|
+
const isLoggedIn = !!user;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (user?.email) {
|
|
28
|
+
setEmail(user.email);
|
|
29
|
+
}
|
|
30
|
+
}, [user]);
|
|
31
|
+
|
|
32
|
+
const steps = [
|
|
33
|
+
{ number: 1, label: 'Details' },
|
|
34
|
+
{ number: 2, label: 'Confirmation' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const handleNext = () => {
|
|
38
|
+
if (!email || !tenantName) {
|
|
39
|
+
toast({
|
|
40
|
+
title: 'Error',
|
|
41
|
+
description: 'Please fill in all fields',
|
|
42
|
+
variant: 'destructive',
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isLoggedIn) {
|
|
48
|
+
if (!password) {
|
|
49
|
+
toast({
|
|
50
|
+
title: 'Error',
|
|
51
|
+
description: 'Please enter a password',
|
|
52
|
+
variant: 'destructive',
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (password.length < 6) {
|
|
57
|
+
toast({
|
|
58
|
+
title: 'Error',
|
|
59
|
+
description: 'Password must be at least 6 characters long',
|
|
60
|
+
variant: 'destructive',
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setCurrentStep(2);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleBack = () => {
|
|
70
|
+
if (currentStep > 1) {
|
|
71
|
+
setCurrentStep(currentStep - 1);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleSubmit = async () => {
|
|
76
|
+
setLoading(true);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
let ownerId: string;
|
|
80
|
+
|
|
81
|
+
if (isLoggedIn) {
|
|
82
|
+
ownerId = user!.id;
|
|
83
|
+
} else {
|
|
84
|
+
const { data: authData, error: authError } = await supabase.auth.signUp({
|
|
85
|
+
email: email,
|
|
86
|
+
password: password,
|
|
87
|
+
options: {
|
|
88
|
+
emailRedirectTo: `${window.location.origin}/`
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (authError) {
|
|
93
|
+
if (authError.code == "user_already_exists") {
|
|
94
|
+
throw new Error('This email address is already registered. Please sign in.');
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(authError.message ?? "An error occurred. Please try again.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!authData.user) {
|
|
101
|
+
throw new Error('User could not be created');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (authData.user.identities && authData.user.identities.length === 0) {
|
|
105
|
+
throw new Error('This email address is already registered. Please sign in.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ownerId = authData.user.id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get session token for API call
|
|
112
|
+
const { data: sessionData } = await supabase.auth.getSession();
|
|
113
|
+
const token = sessionData?.session?.access_token ?? "";
|
|
114
|
+
const tenantId = v4();
|
|
115
|
+
|
|
116
|
+
await registerTenant({
|
|
117
|
+
tenantId,
|
|
118
|
+
name: tenantName,
|
|
119
|
+
ownerId: ownerId,
|
|
120
|
+
}, { token, userId: ownerId, tenantId });
|
|
121
|
+
|
|
122
|
+
toast({
|
|
123
|
+
title: 'Success',
|
|
124
|
+
description: isLoggedIn
|
|
125
|
+
? 'Organization registered successfully!'
|
|
126
|
+
: 'Registration successful! Please check your email to confirm.',
|
|
127
|
+
});
|
|
128
|
+
navigate('/auth');
|
|
129
|
+
} catch (error: any) {
|
|
130
|
+
toast({
|
|
131
|
+
title: 'Error',
|
|
132
|
+
description: !error.message || (Object.keys(error.message)?.length ?? 0) == 0 ? 'Registration failed' : error.message,
|
|
133
|
+
variant: 'destructive',
|
|
134
|
+
});
|
|
135
|
+
} finally {
|
|
136
|
+
setLoading(false);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
|
142
|
+
<Card className="w-full max-w-2xl shadow-[var(--shadow-medium)]">
|
|
143
|
+
<CardContent className="p-8">
|
|
144
|
+
<div className="flex justify-center mb-8">
|
|
145
|
+
<img
|
|
146
|
+
src="/logo.png"
|
|
147
|
+
alt="App Logo"
|
|
148
|
+
className="h-40 w-auto object-contain"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Step Indicator */}
|
|
153
|
+
<div className="flex items-center justify-between mb-8 max-w-md mx-auto">
|
|
154
|
+
{steps.map((step, index) => (
|
|
155
|
+
<div key={step.number} className="flex items-center flex-1">
|
|
156
|
+
<div className="flex flex-col items-center flex-1">
|
|
157
|
+
<div
|
|
158
|
+
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
|
159
|
+
currentStep >= step.number
|
|
160
|
+
? 'bg-primary text-primary-foreground'
|
|
161
|
+
: 'bg-muted text-muted-foreground'
|
|
162
|
+
}`}
|
|
163
|
+
>
|
|
164
|
+
{currentStep > step.number ? (
|
|
165
|
+
<Check className="w-5 h-5" />
|
|
166
|
+
) : (
|
|
167
|
+
step.number
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<span className="text-xs mt-2 text-center">{step.label}</span>
|
|
171
|
+
</div>
|
|
172
|
+
{index < steps.length - 1 && (
|
|
173
|
+
<div
|
|
174
|
+
className={`h-0.5 flex-1 mx-2 transition-colors ${
|
|
175
|
+
currentStep > step.number ? 'bg-primary' : 'bg-muted'
|
|
176
|
+
}`}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Step 1: Details */}
|
|
184
|
+
{currentStep === 1 && (
|
|
185
|
+
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
|
|
186
|
+
<div>
|
|
187
|
+
<h2 className="text-2xl font-semibold mb-2">Enter your details</h2>
|
|
188
|
+
<p className="text-muted-foreground">
|
|
189
|
+
Enter your email address and organization name.
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div className="space-y-4">
|
|
194
|
+
<div className="space-y-2">
|
|
195
|
+
<Label htmlFor="tenantName">Organization name</Label>
|
|
196
|
+
<Input
|
|
197
|
+
id="tenantName"
|
|
198
|
+
type="text"
|
|
199
|
+
placeholder="My Organization"
|
|
200
|
+
value={tenantName}
|
|
201
|
+
onChange={(e) => setTenantName(e.target.value)}
|
|
202
|
+
required
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="space-y-2">
|
|
206
|
+
<Label htmlFor="email">Email</Label>
|
|
207
|
+
<Input
|
|
208
|
+
id="email"
|
|
209
|
+
type="email"
|
|
210
|
+
placeholder="your@email.com"
|
|
211
|
+
value={email}
|
|
212
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
213
|
+
readOnly={isLoggedIn}
|
|
214
|
+
className={isLoggedIn ? 'bg-muted' : ''}
|
|
215
|
+
required
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
{!isLoggedIn && (
|
|
219
|
+
<div className="space-y-2">
|
|
220
|
+
<Label htmlFor="password">Password</Label>
|
|
221
|
+
<Input
|
|
222
|
+
id="password"
|
|
223
|
+
type="password"
|
|
224
|
+
placeholder="At least 6 characters"
|
|
225
|
+
value={password}
|
|
226
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
227
|
+
required
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Step 2: Confirmation */}
|
|
236
|
+
{currentStep === 2 && (
|
|
237
|
+
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
|
|
238
|
+
<div>
|
|
239
|
+
<h2 className="text-2xl font-semibold mb-2">Confirmation</h2>
|
|
240
|
+
<p className="text-muted-foreground">
|
|
241
|
+
Review your details and complete the registration.
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<div className="space-y-4 p-4 rounded-lg bg-muted/50">
|
|
246
|
+
<div>
|
|
247
|
+
<span className="text-sm text-muted-foreground">Organization name</span>
|
|
248
|
+
<p className="font-medium">{tenantName}</p>
|
|
249
|
+
</div>
|
|
250
|
+
<div>
|
|
251
|
+
<span className="text-sm text-muted-foreground">Email</span>
|
|
252
|
+
<p className="font-medium">{email}</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Navigation Buttons */}
|
|
259
|
+
<div className="flex justify-between mt-8">
|
|
260
|
+
<Button
|
|
261
|
+
type="button"
|
|
262
|
+
variant="outline"
|
|
263
|
+
onClick={currentStep === 1 ? () => navigate('/auth') : handleBack}
|
|
264
|
+
>
|
|
265
|
+
<ChevronLeft className="w-4 h-4 mr-1" />
|
|
266
|
+
Back
|
|
267
|
+
</Button>
|
|
268
|
+
|
|
269
|
+
{currentStep < 2 ? (
|
|
270
|
+
<Button onClick={handleNext}>
|
|
271
|
+
Next
|
|
272
|
+
</Button>
|
|
273
|
+
) : (
|
|
274
|
+
<Button onClick={handleSubmit} disabled={loading}>
|
|
275
|
+
{loading ? 'Registering...' : 'Complete registration'}
|
|
276
|
+
</Button>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</CardContent>
|
|
280
|
+
</Card>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export default Register;
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(window, "matchMedia", {
|
|
4
|
+
writable: true,
|
|
5
|
+
value: (query: string) => ({
|
|
6
|
+
matches: false,
|
|
7
|
+
media: query,
|
|
8
|
+
onchange: null,
|
|
9
|
+
addListener: () => {},
|
|
10
|
+
removeListener: () => {},
|
|
11
|
+
addEventListener: () => {},
|
|
12
|
+
removeEventListener: () => {},
|
|
13
|
+
dispatchEvent: () => {},
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
darkMode: ["class"],
|
|
5
|
+
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
|
|
6
|
+
prefix: "",
|
|
7
|
+
theme: {
|
|
8
|
+
container: {
|
|
9
|
+
center: true,
|
|
10
|
+
padding: "2rem",
|
|
11
|
+
screens: {
|
|
12
|
+
"2xl": "1400px",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
extend: {
|
|
16
|
+
colors: {
|
|
17
|
+
border: "hsl(var(--border))",
|
|
18
|
+
input: "hsl(var(--input))",
|
|
19
|
+
ring: "hsl(var(--ring))",
|
|
20
|
+
background: "hsl(var(--background))",
|
|
21
|
+
foreground: "hsl(var(--foreground))",
|
|
22
|
+
primary: {
|
|
23
|
+
DEFAULT: "hsl(var(--primary))",
|
|
24
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
25
|
+
},
|
|
26
|
+
secondary: {
|
|
27
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
28
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
29
|
+
},
|
|
30
|
+
destructive: {
|
|
31
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
32
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
33
|
+
},
|
|
34
|
+
muted: {
|
|
35
|
+
DEFAULT: "hsl(var(--muted))",
|
|
36
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
37
|
+
},
|
|
38
|
+
accent: {
|
|
39
|
+
DEFAULT: "hsl(var(--accent))",
|
|
40
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
41
|
+
},
|
|
42
|
+
popover: {
|
|
43
|
+
DEFAULT: "hsl(var(--popover))",
|
|
44
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
45
|
+
},
|
|
46
|
+
card: {
|
|
47
|
+
DEFAULT: "hsl(var(--card))",
|
|
48
|
+
foreground: "hsl(var(--card-foreground))",
|
|
49
|
+
},
|
|
50
|
+
success: {
|
|
51
|
+
DEFAULT: "hsl(var(--success))",
|
|
52
|
+
foreground: "hsl(var(--success-foreground))",
|
|
53
|
+
},
|
|
54
|
+
warning: {
|
|
55
|
+
DEFAULT: "hsl(var(--warning))",
|
|
56
|
+
foreground: "hsl(var(--warning-foreground))",
|
|
57
|
+
},
|
|
58
|
+
sidebar: {
|
|
59
|
+
DEFAULT: "hsl(var(--sidebar-background))",
|
|
60
|
+
foreground: "hsl(var(--sidebar-foreground))",
|
|
61
|
+
primary: "hsl(var(--sidebar-primary))",
|
|
62
|
+
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
|
63
|
+
accent: "hsl(var(--sidebar-accent))",
|
|
64
|
+
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
|
65
|
+
border: "hsl(var(--sidebar-border))",
|
|
66
|
+
ring: "hsl(var(--sidebar-ring))",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
borderRadius: {
|
|
70
|
+
lg: "var(--radius)",
|
|
71
|
+
md: "calc(var(--radius) - 2px)",
|
|
72
|
+
sm: "calc(var(--radius) - 4px)",
|
|
73
|
+
},
|
|
74
|
+
keyframes: {
|
|
75
|
+
"accordion-down": {
|
|
76
|
+
from: { height: "0" },
|
|
77
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
78
|
+
},
|
|
79
|
+
"accordion-up": {
|
|
80
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
81
|
+
to: { height: "0" },
|
|
82
|
+
},
|
|
83
|
+
"fade-in": {
|
|
84
|
+
from: { opacity: "0", transform: "translateY(10px)" },
|
|
85
|
+
to: { opacity: "1", transform: "translateY(0)" },
|
|
86
|
+
},
|
|
87
|
+
"slide-in": {
|
|
88
|
+
from: { transform: "translateX(-100%)" },
|
|
89
|
+
to: { transform: "translateX(0)" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
animation: {
|
|
93
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
94
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
95
|
+
"fade-in": "fade-in 0.3s ease-out",
|
|
96
|
+
"slide-in": "slide-in 0.3s ease-out",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
plugins: [require("tailwindcss-animate")],
|
|
101
|
+
} satisfies Config;
|