@kurly-growth/growthman 0.1.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.
- package/.claude/settings.local.json +10 -0
- package/CLAUDE.md +58 -0
- package/README.md +23 -0
- package/app/api/endpoints/[id]/route.ts +63 -0
- package/app/api/endpoints/bulk/route.ts +23 -0
- package/app/api/endpoints/import/route.ts +52 -0
- package/app/api/endpoints/route.ts +39 -0
- package/app/api/endpoints/sync/route.ts +80 -0
- package/app/api/mock/[...slug]/route.ts +82 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +125 -0
- package/app/layout.tsx +36 -0
- package/app/page.tsx +184 -0
- package/bin/cli.js +28 -0
- package/components/endpoint-edit-dialog.tsx +181 -0
- package/components/endpoint-table.tsx +133 -0
- package/components/openapi-upload-dialog.tsx +196 -0
- package/components/ui/button.tsx +62 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/sonner.tsx +37 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/textarea.tsx +18 -0
- package/components.json +22 -0
- package/eslint.config.mjs +18 -0
- package/lib/openapi-parser.ts +270 -0
- package/lib/prisma.ts +9 -0
- package/lib/utils.ts +6 -0
- package/lib/validation.ts +19 -0
- package/next.config.ts +7 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +4 -0
- package/postcss.config.mjs +7 -0
- package/prisma/schema.prisma +23 -0
- package/prisma/seed.ts +29 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
- package/types/endpoint.ts +10 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
} from '@/components/ui/dialog'
|
|
11
|
+
import { Button } from '@/components/ui/button'
|
|
12
|
+
import { Input } from '@/components/ui/input'
|
|
13
|
+
import { Label } from '@/components/ui/label'
|
|
14
|
+
import { toast } from 'sonner'
|
|
15
|
+
|
|
16
|
+
interface OpenAPIUploadDialogProps {
|
|
17
|
+
open: boolean
|
|
18
|
+
onOpenChange: (open: boolean) => void
|
|
19
|
+
onImportComplete: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ImportMode = 'url' | 'file'
|
|
23
|
+
|
|
24
|
+
export function OpenAPIUploadDialog({
|
|
25
|
+
open,
|
|
26
|
+
onOpenChange,
|
|
27
|
+
onImportComplete,
|
|
28
|
+
}: OpenAPIUploadDialogProps) {
|
|
29
|
+
const [mode, setMode] = useState<ImportMode>('url')
|
|
30
|
+
const [url, setUrl] = useState('')
|
|
31
|
+
const [file, setFile] = useState<File | null>(null)
|
|
32
|
+
const [loading, setLoading] = useState(false)
|
|
33
|
+
const [error, setError] = useState<string | null>(null)
|
|
34
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
35
|
+
|
|
36
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
37
|
+
const selectedFile = e.target.files?.[0]
|
|
38
|
+
if (selectedFile) {
|
|
39
|
+
setFile(selectedFile)
|
|
40
|
+
setError(null)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleImport = async () => {
|
|
45
|
+
setLoading(true)
|
|
46
|
+
setError(null)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
let response: Response
|
|
50
|
+
|
|
51
|
+
if (mode === 'url') {
|
|
52
|
+
if (!url.trim()) {
|
|
53
|
+
throw new Error('Please enter a URL')
|
|
54
|
+
}
|
|
55
|
+
response = await fetch('/api/endpoints/sync', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ url: url.trim() }),
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
if (!file) {
|
|
62
|
+
throw new Error('Please select a file')
|
|
63
|
+
}
|
|
64
|
+
const text = await file.text()
|
|
65
|
+
const spec = JSON.parse(text)
|
|
66
|
+
response = await fetch('/api/endpoints/import', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify(spec),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await response.json()
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(data.error || 'Import failed')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const succeeded = data.succeeded ?? data.created
|
|
80
|
+
const failed = data.failed ?? 0
|
|
81
|
+
if (failed > 0) {
|
|
82
|
+
toast.warning(`Imported ${succeeded} endpoints (${failed} failed)`)
|
|
83
|
+
} else {
|
|
84
|
+
toast.success(`Successfully imported ${succeeded} endpoints`)
|
|
85
|
+
}
|
|
86
|
+
onImportComplete()
|
|
87
|
+
handleClose()
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : 'Import failed'
|
|
90
|
+
setError(message)
|
|
91
|
+
toast.error(message)
|
|
92
|
+
} finally {
|
|
93
|
+
setLoading(false)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleClose = () => {
|
|
98
|
+
setUrl('')
|
|
99
|
+
setFile(null)
|
|
100
|
+
setError(null)
|
|
101
|
+
onOpenChange(false)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const canImport = mode === 'url' ? url.trim().length > 0 : file !== null
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Dialog open={open} onOpenChange={handleClose}>
|
|
108
|
+
<DialogContent className="sm:max-w-lg">
|
|
109
|
+
<DialogHeader>
|
|
110
|
+
<DialogTitle>Import OpenAPI Specification</DialogTitle>
|
|
111
|
+
</DialogHeader>
|
|
112
|
+
|
|
113
|
+
<div className="py-4 space-y-4">
|
|
114
|
+
{/* Mode Toggle */}
|
|
115
|
+
<div className="flex gap-2">
|
|
116
|
+
<Button
|
|
117
|
+
type="button"
|
|
118
|
+
variant={mode === 'url' ? 'default' : 'outline'}
|
|
119
|
+
size="sm"
|
|
120
|
+
onClick={() => setMode('url')}
|
|
121
|
+
>
|
|
122
|
+
From URL
|
|
123
|
+
</Button>
|
|
124
|
+
<Button
|
|
125
|
+
type="button"
|
|
126
|
+
variant={mode === 'file' ? 'default' : 'outline'}
|
|
127
|
+
size="sm"
|
|
128
|
+
onClick={() => setMode('file')}
|
|
129
|
+
>
|
|
130
|
+
From File
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* URL Input */}
|
|
135
|
+
{mode === 'url' && (
|
|
136
|
+
<div className="space-y-2">
|
|
137
|
+
<Label htmlFor="url">OpenAPI JSON URL</Label>
|
|
138
|
+
<Input
|
|
139
|
+
id="url"
|
|
140
|
+
type="url"
|
|
141
|
+
placeholder="https://api.example.com/api-docs-json"
|
|
142
|
+
value={url}
|
|
143
|
+
onChange={(e) => {
|
|
144
|
+
setUrl(e.target.value)
|
|
145
|
+
setError(null)
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
<p className="text-xs text-gray-500">
|
|
149
|
+
Enter the URL to your OpenAPI/Swagger JSON specification
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* File Upload */}
|
|
155
|
+
{mode === 'file' && (
|
|
156
|
+
<div
|
|
157
|
+
className="border-2 border-dashed rounded-lg p-8 text-center cursor-pointer hover:border-gray-400 transition-colors"
|
|
158
|
+
onClick={() => fileInputRef.current?.click()}
|
|
159
|
+
>
|
|
160
|
+
<input
|
|
161
|
+
ref={fileInputRef}
|
|
162
|
+
type="file"
|
|
163
|
+
accept=".json"
|
|
164
|
+
onChange={handleFileChange}
|
|
165
|
+
className="hidden"
|
|
166
|
+
/>
|
|
167
|
+
{file ? (
|
|
168
|
+
<p className="text-sm">
|
|
169
|
+
Selected: <span className="font-medium">{file.name}</span>
|
|
170
|
+
</p>
|
|
171
|
+
) : (
|
|
172
|
+
<p className="text-gray-500">
|
|
173
|
+
Click to select an OpenAPI JSON file
|
|
174
|
+
</p>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* Error */}
|
|
180
|
+
{error && (
|
|
181
|
+
<p className="text-red-500 text-sm">{error}</p>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<DialogFooter>
|
|
186
|
+
<Button variant="outline" onClick={handleClose}>
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
<Button onClick={handleImport} disabled={!canImport || loading}>
|
|
190
|
+
{loading ? 'Importing...' : 'Import'}
|
|
191
|
+
</Button>
|
|
192
|
+
</DialogFooter>
|
|
193
|
+
</DialogContent>
|
|
194
|
+
</Dialog>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
size = "default",
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
data-size={size}
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
5
|
+
import { CheckIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function Checkbox({
|
|
10
|
+
className,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<CheckboxPrimitive.Root
|
|
15
|
+
data-slot="checkbox"
|
|
16
|
+
className={cn(
|
|
17
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<CheckboxPrimitive.Indicator
|
|
23
|
+
data-slot="checkbox-indicator"
|
|
24
|
+
className="grid place-content-center text-current transition-none"
|
|
25
|
+
>
|
|
26
|
+
<CheckIcon className="size-3.5" />
|
|
27
|
+
</CheckboxPrimitive.Indicator>
|
|
28
|
+
</CheckboxPrimitive.Root>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { Checkbox }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
5
|
+
import { XIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function Dialog({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
12
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function DialogTrigger({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
18
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function DialogPortal({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
24
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function DialogClose({
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
30
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function DialogOverlay({
|
|
34
|
+
className,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
37
|
+
return (
|
|
38
|
+
<DialogPrimitive.Overlay
|
|
39
|
+
data-slot="dialog-overlay"
|
|
40
|
+
className={cn(
|
|
41
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function DialogContent({
|
|
50
|
+
className,
|
|
51
|
+
children,
|
|
52
|
+
showCloseButton = true,
|
|
53
|
+
...props
|
|
54
|
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
55
|
+
showCloseButton?: boolean
|
|
56
|
+
}) {
|
|
57
|
+
return (
|
|
58
|
+
<DialogPortal data-slot="dialog-portal">
|
|
59
|
+
<DialogOverlay />
|
|
60
|
+
<DialogPrimitive.Content
|
|
61
|
+
data-slot="dialog-content"
|
|
62
|
+
className={cn(
|
|
63
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
{showCloseButton && (
|
|
70
|
+
<DialogPrimitive.Close
|
|
71
|
+
data-slot="dialog-close"
|
|
72
|
+
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
73
|
+
>
|
|
74
|
+
<XIcon />
|
|
75
|
+
<span className="sr-only">Close</span>
|
|
76
|
+
</DialogPrimitive.Close>
|
|
77
|
+
)}
|
|
78
|
+
</DialogPrimitive.Content>
|
|
79
|
+
</DialogPortal>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
data-slot="dialog-header"
|
|
87
|
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-slot="dialog-footer"
|
|
97
|
+
className={cn(
|
|
98
|
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function DialogTitle({
|
|
107
|
+
className,
|
|
108
|
+
...props
|
|
109
|
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
110
|
+
return (
|
|
111
|
+
<DialogPrimitive.Title
|
|
112
|
+
data-slot="dialog-title"
|
|
113
|
+
className={cn("text-lg leading-none font-semibold", className)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function DialogDescription({
|
|
120
|
+
className,
|
|
121
|
+
...props
|
|
122
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
123
|
+
return (
|
|
124
|
+
<DialogPrimitive.Description
|
|
125
|
+
data-slot="dialog-description"
|
|
126
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Dialog,
|
|
134
|
+
DialogClose,
|
|
135
|
+
DialogContent,
|
|
136
|
+
DialogDescription,
|
|
137
|
+
DialogFooter,
|
|
138
|
+
DialogHeader,
|
|
139
|
+
DialogOverlay,
|
|
140
|
+
DialogPortal,
|
|
141
|
+
DialogTitle,
|
|
142
|
+
DialogTrigger,
|
|
143
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
13
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Label({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<LabelPrimitive.Root
|
|
14
|
+
data-slot="label"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Label }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CircleCheckIcon,
|
|
5
|
+
InfoIcon,
|
|
6
|
+
Loader2Icon,
|
|
7
|
+
OctagonXIcon,
|
|
8
|
+
TriangleAlertIcon,
|
|
9
|
+
} from "lucide-react"
|
|
10
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
11
|
+
|
|
12
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<Sonner
|
|
15
|
+
theme="light"
|
|
16
|
+
className="toaster group"
|
|
17
|
+
icons={{
|
|
18
|
+
success: <CircleCheckIcon className="size-4" />,
|
|
19
|
+
info: <InfoIcon className="size-4" />,
|
|
20
|
+
warning: <TriangleAlertIcon className="size-4" />,
|
|
21
|
+
error: <OctagonXIcon className="size-4" />,
|
|
22
|
+
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
23
|
+
}}
|
|
24
|
+
style={
|
|
25
|
+
{
|
|
26
|
+
"--normal-bg": "var(--popover)",
|
|
27
|
+
"--normal-text": "var(--popover-foreground)",
|
|
28
|
+
"--normal-border": "var(--border)",
|
|
29
|
+
"--border-radius": "var(--radius)",
|
|
30
|
+
} as React.CSSProperties
|
|
31
|
+
}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { Toaster }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
data-slot="table-container"
|
|
11
|
+
className="relative w-full overflow-x-auto"
|
|
12
|
+
>
|
|
13
|
+
<table
|
|
14
|
+
data-slot="table"
|
|
15
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
23
|
+
return (
|
|
24
|
+
<thead
|
|
25
|
+
data-slot="table-header"
|
|
26
|
+
className={cn("[&_tr]:border-b", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
33
|
+
return (
|
|
34
|
+
<tbody
|
|
35
|
+
data-slot="table-body"
|
|
36
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
43
|
+
return (
|
|
44
|
+
<tfoot
|
|
45
|
+
data-slot="table-footer"
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
56
|
+
return (
|
|
57
|
+
<tr
|
|
58
|
+
data-slot="table-row"
|
|
59
|
+
className={cn(
|
|
60
|
+
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
69
|
+
return (
|
|
70
|
+
<th
|
|
71
|
+
data-slot="table-head"
|
|
72
|
+
className={cn(
|
|
73
|
+
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
82
|
+
return (
|
|
83
|
+
<td
|
|
84
|
+
data-slot="table-cell"
|
|
85
|
+
className={cn(
|
|
86
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function TableCaption({
|
|
95
|
+
className,
|
|
96
|
+
...props
|
|
97
|
+
}: React.ComponentProps<"caption">) {
|
|
98
|
+
return (
|
|
99
|
+
<caption
|
|
100
|
+
data-slot="table-caption"
|
|
101
|
+
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
Table,
|
|
109
|
+
TableHeader,
|
|
110
|
+
TableBody,
|
|
111
|
+
TableFooter,
|
|
112
|
+
TableHead,
|
|
113
|
+
TableRow,
|
|
114
|
+
TableCell,
|
|
115
|
+
TableCaption,
|
|
116
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
package/components.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|