nobalmako 1.0.0

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.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. package/tsconfig.json +34 -0
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ AlertDialog,
8
+ AlertDialogAction,
9
+ AlertDialogCancel,
10
+ AlertDialogContent,
11
+ AlertDialogDescription,
12
+ AlertDialogFooter,
13
+ AlertDialogHeader,
14
+ AlertDialogTitle,
15
+ AlertDialogTrigger,
16
+ } from "@/components/ui/alert-dialog";
17
+ import { Trash2, AlertTriangle, ShieldAlert, Skull } from "lucide-react";
18
+ import { useTeams } from "@/hooks/use-teams";
19
+
20
+ interface DeleteTeamDialogProps {
21
+ teamId: string;
22
+ teamName: string;
23
+ children?: React.ReactNode;
24
+ }
25
+
26
+ export default function DeleteTeamDialog({ teamId, teamName, children }: DeleteTeamDialogProps) {
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState("");
29
+ const [open, setOpen] = useState(false);
30
+ const queryClient = useQueryClient();
31
+
32
+ const handleDelete = async (e: React.MouseEvent) => {
33
+ e.preventDefault();
34
+ setError("");
35
+ setIsLoading(true);
36
+
37
+ try {
38
+ const response = await fetch('/api/teams', {
39
+ method: 'DELETE',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({ id: teamId }),
44
+ });
45
+
46
+ const data = await response.json();
47
+
48
+ if (response.ok) {
49
+ setOpen(false);
50
+ queryClient.invalidateQueries({ queryKey: ['teams'] });
51
+ } else {
52
+ setError(data.error || "Failed to delete team");
53
+ }
54
+ } catch (err) {
55
+ setError("An unexpected error occurred");
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <AlertDialog open={open} onOpenChange={setOpen}>
63
+ <AlertDialogTrigger asChild>
64
+ {children || (
65
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:text-destructive hover:bg-destructive/10 transition-colors">
66
+ <Trash2 className="h-4 w-4" />
67
+ </Button>
68
+ )}
69
+ </AlertDialogTrigger>
70
+ <AlertDialogContent className="sm:max-w-[450px]">
71
+ <AlertDialogHeader>
72
+ <div className="flex items-center gap-3 mb-2">
73
+ <div className="p-2 bg-destructive/10 rounded-full text-destructive">
74
+ <Skull className="h-5 w-5" />
75
+ </div>
76
+ <AlertDialogTitle className="text-xl">Delete Project Permanently</AlertDialogTitle>
77
+ </div>
78
+ <AlertDialogDescription className="text-base text-muted-foreground pt-1">
79
+ Are you sure you want to delete <span className="font-bold text-foreground bg-muted px-2 rounded">&quot;{teamName}&quot;</span>?
80
+ <div className="mt-4 p-4 bg-destructive/5 rounded-lg border border-destructive/20 text-sm text-destructive font-medium space-y-3">
81
+ <div className="flex gap-2">
82
+ <AlertTriangle className="h-5 w-5 shrink-0" />
83
+ <p>CRITICAL: This will immediately and permanently destroy:</p>
84
+ </div>
85
+ <ul className="list-disc list-inside space-y-1 pl-7 opacity-90 font-normal">
86
+ <li>All project configurations and metadata</li>
87
+ <li>Every environment (Prod, Staging, etc.)</li>
88
+ <li>ALL encrypted secrets and variables</li>
89
+ <li>Project audit logs and activity history</li>
90
+ </ul>
91
+ </div>
92
+ </AlertDialogDescription>
93
+ </AlertDialogHeader>
94
+ {error && (
95
+ <div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 my-2">
96
+ {error}
97
+ </div>
98
+ )}
99
+ <AlertDialogFooter className="pt-6 gap-2">
100
+ <AlertDialogCancel disabled={isLoading} className="mt-0">Cancel</AlertDialogCancel>
101
+ <AlertDialogAction
102
+ onClick={handleDelete}
103
+ disabled={isLoading}
104
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-8 shadow-sm shadow-destructive/20 border-0"
105
+ >
106
+ {isLoading ? "Deleting..." : "Delete Full Project"}
107
+ </AlertDialogAction>
108
+ </AlertDialogFooter>
109
+ </AlertDialogContent>
110
+ </AlertDialog>
111
+ );
112
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogTitle,
14
+ AlertDialogTrigger,
15
+ } from "@/components/ui/alert-dialog";
16
+ import { Trash2, AlertTriangle } from "lucide-react";
17
+ import { useVariables } from "@/hooks/use-variables";
18
+ import { useQueryClient } from "@tanstack/react-query";
19
+
20
+ interface DeleteVariableDialogProps {
21
+ variableId: string;
22
+ variableKey: string;
23
+ children?: React.ReactNode;
24
+ }
25
+
26
+ export default function DeleteVariableDialog({ variableId, variableKey, children }: DeleteVariableDialogProps) {
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState("");
29
+ const [open, setOpen] = useState(false);
30
+ const queryClient = useQueryClient();
31
+
32
+ const handleDelete = async (e: React.MouseEvent) => {
33
+ e.preventDefault();
34
+ setError("");
35
+ setIsLoading(true);
36
+
37
+ try {
38
+ const response = await fetch('/api/variables', {
39
+ method: 'DELETE',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ body: JSON.stringify({ id: variableId }),
44
+ });
45
+
46
+ const data = await response.json();
47
+
48
+ if (response.ok) {
49
+ setOpen(false);
50
+ queryClient.invalidateQueries({ queryKey: ["variables"] });
51
+ } else {
52
+ setError(data.error || "Failed to delete variable");
53
+ }
54
+ } catch (err) {
55
+ setError("An unexpected error occurred");
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <AlertDialog open={open} onOpenChange={setOpen}>
63
+ <AlertDialogTrigger asChild>
64
+ {children || (
65
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:text-destructive hover:bg-destructive/10 transition-colors">
66
+ <Trash2 className="h-4 w-4" />
67
+ </Button>
68
+ )}
69
+ </AlertDialogTrigger>
70
+ <AlertDialogContent className="sm:max-w-[425px]">
71
+ <AlertDialogHeader>
72
+ <div className="flex items-center gap-3 mb-2">
73
+ <div className="p-2 bg-destructive/10 rounded-full">
74
+ <AlertTriangle className="h-5 w-5 text-destructive" />
75
+ </div>
76
+ <AlertDialogTitle className="text-xl">Delete Variable</AlertDialogTitle>
77
+ </div>
78
+ <AlertDialogDescription className="text-base text-muted-foreground pt-1">
79
+ Are you sure you want to delete the variable <span className="font-mono font-bold text-foreground bg-muted px-1.5 rounded">&quot;{variableKey}&quot;</span>?
80
+ <div className="mt-3 p-3 bg-muted/50 rounded-lg border border-muted text-sm italic">
81
+ All historical values and logs for this variable will be permanently removed.
82
+ </div>
83
+ </AlertDialogDescription>
84
+ </AlertDialogHeader>
85
+ {error && (
86
+ <div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20 my-2">
87
+ {error}
88
+ </div>
89
+ )}
90
+ <AlertDialogFooter className="pt-4 gap-2">
91
+ <AlertDialogCancel disabled={isLoading} className="mt-0">Cancel</AlertDialogCancel>
92
+ <AlertDialogAction
93
+ onClick={handleDelete}
94
+ disabled={isLoading}
95
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-6 shadow-sm shadow-destructive/20 border-0"
96
+ >
97
+ {isLoading ? "Deleting..." : "Delete Permanently"}
98
+ </AlertDialogAction>
99
+ </AlertDialogFooter>
100
+ </AlertDialogContent>
101
+ </AlertDialog>
102
+ );
103
+ }
@@ -0,0 +1,202 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "@/components/ui/dialog";
18
+ import { Edit, Globe, Palette, Type, AlignLeft, CheckCircle2 } from "lucide-react";
19
+ import { useEnvironments } from "@/hooks/use-environments";
20
+ import { Checkbox } from "@/components/ui/checkbox";
21
+
22
+ interface EditEnvironmentDialogProps {
23
+ environmentId: string;
24
+ children?: React.ReactNode;
25
+ }
26
+
27
+ export default function EditEnvironmentDialog({ environmentId, children }: EditEnvironmentDialogProps) {
28
+ const [open, setOpen] = useState(false);
29
+ const [name, setName] = useState("");
30
+ const [description, setDescription] = useState("");
31
+ const [color, setColor] = useState("#3b82f6");
32
+ const [isDefault, setIsDefault] = useState(false);
33
+ const [isLoading, setIsLoading] = useState(false);
34
+ const [error, setError] = useState("");
35
+ const { environments } = useEnvironments();
36
+ const queryClient = useQueryClient();
37
+
38
+ const environment = environments?.find(e => e.id === environmentId);
39
+
40
+ useEffect(() => {
41
+ if (environment && open) {
42
+ setName(environment.name);
43
+ setDescription(environment.description || "");
44
+ setColor(environment.color || "#3b82f6");
45
+ setIsDefault(environment.isDefault || false);
46
+ }
47
+ }, [environment, open]);
48
+
49
+ const handleSubmit = async (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ setError("");
52
+ setIsLoading(true);
53
+
54
+ try {
55
+ const response = await fetch('/api/environments', {
56
+ method: 'PUT',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ },
60
+ body: JSON.stringify({ id: environmentId, name, description, color, isDefault }),
61
+ });
62
+
63
+ const data = await response.json();
64
+
65
+ if (response.ok) {
66
+ setOpen(false);
67
+ queryClient.invalidateQueries({ queryKey: ["environments"] });
68
+ } else {
69
+ setError(data.error || "Failed to update environment");
70
+ }
71
+ } catch (err) {
72
+ setError("An unexpected error occurred");
73
+ } finally {
74
+ setIsLoading(false);
75
+ }
76
+ };
77
+
78
+ return (
79
+ <Dialog open={open} onOpenChange={setOpen}>
80
+ <DialogTrigger asChild>
81
+ {children || (
82
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
83
+ <Edit className="h-4 w-4" />
84
+ </Button>
85
+ )}
86
+ </DialogTrigger>
87
+ <DialogContent className="sm:max-w-[425px]">
88
+ <DialogHeader>
89
+ <div className="flex items-center gap-2 mb-1">
90
+ <div className="p-1.5 bg-primary/10 rounded-md">
91
+ <Globe className="h-5 w-5 text-primary" />
92
+ </div>
93
+ <DialogTitle className="text-xl">Edit Environment</DialogTitle>
94
+ </div>
95
+ <DialogDescription>
96
+ Update configuration and appearance for the {environment?.name} stage.
97
+ </DialogDescription>
98
+ </DialogHeader>
99
+ <form onSubmit={handleSubmit} className="space-y-6 pt-4">
100
+ {error && (
101
+ <div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
102
+ {error}
103
+ </div>
104
+ )}
105
+
106
+ <div className="space-y-4">
107
+ <div className="grid gap-2">
108
+ <Label htmlFor="name" className="flex items-center gap-2">
109
+ <Type className="h-3.5 w-3.5 text-muted-foreground" />
110
+ Environment Name
111
+ </Label>
112
+ <Input
113
+ id="name"
114
+ value={name}
115
+ onChange={(e) => setName(e.target.value)}
116
+ placeholder="e.g. production, staging"
117
+ required
118
+ disabled={isLoading}
119
+ className="bg-muted/30 border-muted"
120
+ />
121
+ </div>
122
+
123
+ <div className="grid gap-2">
124
+ <Label htmlFor="description" className="flex items-center gap-2">
125
+ <AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
126
+ Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
127
+ </Label>
128
+ <Textarea
129
+ id="description"
130
+ value={description}
131
+ onChange={(e) => setDescription(e.target.value)}
132
+ className="resize-none min-h-[100px] bg-muted/30 border-muted"
133
+ disabled={isLoading}
134
+ />
135
+ </div>
136
+
137
+ <div className="grid gap-2">
138
+ <Label htmlFor="color" className="flex items-center gap-2">
139
+ <Palette className="h-3.5 w-3.5 text-muted-foreground" />
140
+ Environment Color
141
+ </Label>
142
+ <div className="flex items-center gap-4">
143
+ <Input
144
+ id="color"
145
+ type="color"
146
+ value={color}
147
+ onChange={(e) => setColor(e.target.value)}
148
+ className="w-14 h-11 p-1 rounded-md border-muted cursor-pointer"
149
+ disabled={isLoading}
150
+ />
151
+ <div className="flex-1 px-3 py-2 bg-muted/30 border border-muted rounded-md flex items-center justify-between">
152
+ <span className="text-sm font-mono uppercase font-semibold text-muted-foreground">
153
+ {color}
154
+ </span>
155
+ <div
156
+ className="w-4 h-4 rounded-full shadow-sm"
157
+ style={{ backgroundColor: color }}
158
+ />
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="flex items-center space-x-3 p-3 bg-muted/30 rounded-lg border border-muted">
164
+ <Checkbox
165
+ id="isDefault"
166
+ checked={isDefault}
167
+ onCheckedChange={(checked) => setIsDefault(!!checked)}
168
+ disabled={isLoading}
169
+ />
170
+ <div className="grid gap-1.5 leading-none">
171
+ <Label
172
+ htmlFor="isDefault"
173
+ className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2"
174
+ >
175
+ <CheckCircle2 className="h-3.5 w-3.5 text-primary" />
176
+ Set as Primary
177
+ </Label>
178
+ <p className="text-xs text-muted-foreground">
179
+ Variables are assigned to the primary environment by default.
180
+ </p>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <DialogFooter className="pt-2">
186
+ <Button
187
+ type="button"
188
+ variant="ghost"
189
+ onClick={() => setOpen(false)}
190
+ disabled={isLoading}
191
+ >
192
+ Cancel
193
+ </Button>
194
+ <Button type="submit" disabled={isLoading} className="px-8 shadow-sm">
195
+ {isLoading ? "Updating..." : "Update Environment"}
196
+ </Button>
197
+ </DialogFooter>
198
+ </form>
199
+ </DialogContent>
200
+ </Dialog>
201
+ );
202
+ }
@@ -0,0 +1,143 @@
1
+
2
+ 'use client';
3
+
4
+ import { useState } from "react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ } from "@/components/ui/dialog";
17
+ import { Edit, User, Mail, Shield, UserRoundPen } from "lucide-react";
18
+ import { useQueryClient } from "@tanstack/react-query";
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectItem,
23
+ SelectTrigger,
24
+ SelectValue,
25
+ } from "@/components/ui/select";
26
+
27
+ interface EditMemberDialogProps {
28
+ teamId: string;
29
+ member: {
30
+ id: string;
31
+ role: string;
32
+ user: {
33
+ name: string;
34
+ email: string;
35
+ };
36
+ };
37
+ children?: React.ReactNode;
38
+ }
39
+
40
+ export default function EditMemberDialog({ teamId, member, children }: EditMemberDialogProps) {
41
+ const [open, setOpen] = useState(false);
42
+ const [role, setRole] = useState(member.role);
43
+ const [isLoading, setIsLoading] = useState(false);
44
+ const [error, setError] = useState("");
45
+ const queryClient = useQueryClient();
46
+
47
+ const handleSubmit = async (e: React.FormEvent) => {
48
+ e.preventDefault();
49
+ setError("");
50
+ setIsLoading(true);
51
+
52
+ try {
53
+ const response = await fetch('/api/team-members', {
54
+ method: 'PUT',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ },
58
+ body: JSON.stringify({ teamId, memberId: member.id, role }),
59
+ });
60
+
61
+ const data = await response.json();
62
+
63
+ if (response.ok) {
64
+ setOpen(false);
65
+ queryClient.invalidateQueries({ queryKey: ["team-members", teamId] });
66
+ } else {
67
+ setError(data.error || "Failed to update member");
68
+ }
69
+ } catch (err) {
70
+ setError("An unexpected error occurred");
71
+ } finally {
72
+ setIsLoading(false);
73
+ }
74
+ };
75
+
76
+ return (
77
+ <Dialog open={open} onOpenChange={setOpen}>
78
+ <DialogTrigger asChild>
79
+ {children || (
80
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
81
+ <Edit className="h-4 w-4" />
82
+ </Button>
83
+ )}
84
+ </DialogTrigger>
85
+ <DialogContent className="sm:max-w-[425px]">
86
+ <DialogHeader>
87
+ <div className="flex items-center gap-2 mb-1">
88
+ <div className="p-1.5 bg-primary/10 rounded-md">
89
+ <UserRoundPen className="h-5 w-5 text-primary" />
90
+ </div>
91
+ <DialogTitle className="text-xl">Edit Member Role</DialogTitle>
92
+ </div>
93
+ <DialogDescription>
94
+ Change role for {member.user.name} ({member.user.email})
95
+ </DialogDescription>
96
+ </DialogHeader>
97
+
98
+ <form onSubmit={handleSubmit} className="space-y-4 py-4">
99
+ {error && (
100
+ <div className="p-3 text-sm font-medium text-destructive bg-destructive/10 rounded-lg border border-destructive/20 text-center">
101
+ {error}
102
+ </div>
103
+ )}
104
+
105
+ <div className="space-y-2">
106
+ <Label htmlFor="role" className="text-sm font-medium text-slate-700">Role</Label>
107
+ <Select value={role} onValueChange={setRole}>
108
+ <SelectTrigger className="bg-white border-slate-200">
109
+ <SelectValue placeholder="Select role" />
110
+ </SelectTrigger>
111
+ <SelectContent>
112
+ <SelectItem value="member">Member</SelectItem>
113
+ <SelectItem value="admin">Admin</SelectItem>
114
+ <SelectItem value="owner">Owner</SelectItem>
115
+ </SelectContent>
116
+ </Select>
117
+ <p className="text-[11px] text-slate-500 mt-1">
118
+ Admin can manage environments and secrets. Owner can also manage team members.
119
+ </p>
120
+ </div>
121
+
122
+ <DialogFooter className="pt-2">
123
+ <Button
124
+ type="button"
125
+ variant="outline"
126
+ onClick={() => setOpen(false)}
127
+ className="border-slate-200 hover:bg-slate-50"
128
+ >
129
+ Cancel
130
+ </Button>
131
+ <Button
132
+ type="submit"
133
+ disabled={isLoading || role === member.role}
134
+ className="bg-slate-950 hover:bg-slate-800 text-white min-w-[100px]"
135
+ >
136
+ {isLoading ? "Saving..." : "Save Changes"}
137
+ </Button>
138
+ </DialogFooter>
139
+ </form>
140
+ </DialogContent>
141
+ </Dialog>
142
+ );
143
+ }