create-vexcms 0.0.3
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/index.d.ts +1 -0
- package/dist/index.js +2409 -0
- package/package.json +37 -0
- package/templates/base-nextjs/.prettierignore +26 -0
- package/templates/base-nextjs/.prettierrc +7 -0
- package/templates/base-nextjs/README.md +256 -0
- package/templates/base-nextjs/_gitignore +2 -0
- package/templates/base-nextjs/components.json +24 -0
- package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
- package/templates/base-nextjs/convex/_generated/api.js +23 -0
- package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
- package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
- package/templates/base-nextjs/convex/_generated/server.js +93 -0
- package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
- package/templates/base-nextjs/convex/auth/api.ts +8 -0
- package/templates/base-nextjs/convex/auth/config.ts +10 -0
- package/templates/base-nextjs/convex/auth/db.ts +303 -0
- package/templates/base-nextjs/convex/auth/index.ts +14 -0
- package/templates/base-nextjs/convex/auth/options.ts +63 -0
- package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
- package/templates/base-nextjs/convex/auth.config.ts +7 -0
- package/templates/base-nextjs/convex/convex.config.ts +5 -0
- package/templates/base-nextjs/convex/http.ts +28 -0
- package/templates/base-nextjs/convex/schema.ts +3 -0
- package/templates/base-nextjs/convex/vex/auth.ts +38 -0
- package/templates/base-nextjs/convex/vex/collections.ts +321 -0
- package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
- package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
- package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
- package/templates/base-nextjs/convex/vex/media.ts +246 -0
- package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
- package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
- package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
- package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
- package/templates/base-nextjs/convex/vex/versions.ts +588 -0
- package/templates/base-nextjs/eslint.config.mjs +164 -0
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +67 -0
- package/templates/base-nextjs/postcss.config.mjs +7 -0
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/public/file.svg +1 -0
- package/templates/base-nextjs/public/globe.svg +1 -0
- package/templates/base-nextjs/public/next.svg +1 -0
- package/templates/base-nextjs/public/vercel.svg +1 -0
- package/templates/base-nextjs/public/window.svg +1 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
- package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
- package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/globals.css +130 -0
- package/templates/base-nextjs/src/app/layout.tsx +30 -0
- package/templates/base-nextjs/src/auth/client.tsx +41 -0
- package/templates/base-nextjs/src/auth/permissions.ts +102 -0
- package/templates/base-nextjs/src/auth/server.ts +16 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
- package/templates/base-nextjs/src/auth/types.ts +3 -0
- package/templates/base-nextjs/src/components/component-example.tsx +474 -0
- package/templates/base-nextjs/src/components/example.tsx +52 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
- package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
- package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
- package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
- package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
- package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
- package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
- package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
- package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
- package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
- package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
- package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
- package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
- package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
- package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
- package/templates/base-nextjs/src/db/constants/index.ts +44 -0
- package/templates/base-nextjs/src/db/types.ts +6 -0
- package/templates/base-nextjs/src/env.mjs +48 -0
- package/templates/base-nextjs/src/lib/utils.ts +6 -0
- package/templates/base-nextjs/src/proxy.ts +23 -0
- package/templates/base-nextjs/src/vexcms/access.ts +28 -0
- package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
- package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
- package/templates/base-nextjs/tsconfig.json +60 -0
- package/templates/base-nextjs/vex.config.ts +23 -0
- package/templates/marketing-site/convex/pages.ts +46 -0
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
- package/templates/marketing-site/src/db/constants/index.ts +52 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
- package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
- package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
- package/templates/marketing-site/vex.config.ts +32 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { Button } from "~/components/ui/button"
|
|
7
|
+
import { Input } from "~/components/ui/input"
|
|
8
|
+
import { Textarea } from "~/components/ui/textarea"
|
|
9
|
+
import { cn } from "~/lib/utils"
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={cn(
|
|
15
|
+
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-9 rounded-md border shadow-xs transition-[color,box-shadow] has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
data-slot="input-group"
|
|
19
|
+
role="group"
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inputGroupAddonVariants = cva(
|
|
26
|
+
"text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
|
|
27
|
+
{
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
align: "inline-start",
|
|
30
|
+
},
|
|
31
|
+
variants: {
|
|
32
|
+
align: {
|
|
33
|
+
"block-end":
|
|
34
|
+
"px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
|
|
35
|
+
"block-start":
|
|
36
|
+
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
|
|
37
|
+
"inline-end": "pr-2 has-[>button]:mr-[-0.25rem] has-[>kbd]:mr-[-0.15rem] order-last",
|
|
38
|
+
"inline-start": "pl-2 has-[>button]:ml-[-0.25rem] has-[>kbd]:ml-[-0.15rem] order-first",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
function InputGroupAddon({
|
|
45
|
+
align = "inline-start",
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
52
|
+
data-align={align}
|
|
53
|
+
data-slot="input-group-addon"
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
59
|
+
}}
|
|
60
|
+
role="group"
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const inputGroupButtonVariants = cva("gap-2 text-sm shadow-none flex items-center", {
|
|
67
|
+
defaultVariants: {
|
|
68
|
+
size: "xs",
|
|
69
|
+
},
|
|
70
|
+
variants: {
|
|
71
|
+
size: {
|
|
72
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
73
|
+
"icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
74
|
+
sm: "",
|
|
75
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
function InputGroupButton({
|
|
81
|
+
type = "button",
|
|
82
|
+
className,
|
|
83
|
+
size = "xs",
|
|
84
|
+
variant = "ghost",
|
|
85
|
+
...props
|
|
86
|
+
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
|
87
|
+
VariantProps<typeof inputGroupButtonVariants> & {
|
|
88
|
+
type?: "button" | "reset" | "submit"
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<Button
|
|
92
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
93
|
+
data-size={size}
|
|
94
|
+
type={type}
|
|
95
|
+
variant={variant}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
|
|
102
|
+
return (
|
|
103
|
+
<Input
|
|
104
|
+
className={cn(
|
|
105
|
+
"rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1",
|
|
106
|
+
className
|
|
107
|
+
)}
|
|
108
|
+
data-slot="input-group-control"
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
115
|
+
return (
|
|
116
|
+
<span
|
|
117
|
+
className={cn(
|
|
118
|
+
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
127
|
+
return (
|
|
128
|
+
<Textarea
|
|
129
|
+
className={cn(
|
|
130
|
+
"rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none",
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
data-slot="input-group-control"
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
InputGroup,
|
|
141
|
+
InputGroupAddon,
|
|
142
|
+
InputGroupButton,
|
|
143
|
+
InputGroupInput,
|
|
144
|
+
InputGroupText,
|
|
145
|
+
InputGroupTextarea,
|
|
146
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Input as InputPrimitive } from "@base-ui/react/input"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
import { cn } from "~/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Input({ type, className, ...props }: React.ComponentProps<"input">) {
|
|
7
|
+
return (
|
|
8
|
+
<InputPrimitive
|
|
9
|
+
className={cn(
|
|
10
|
+
"dark:bg-input/30 border-input 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:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
data-slot="input"
|
|
14
|
+
type={type}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Input }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "~/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
8
|
+
return (
|
|
9
|
+
<label
|
|
10
|
+
className={cn(
|
|
11
|
+
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
data-slot="label"
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Label }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
5
|
+
import * as React from "react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "~/lib/utils"
|
|
8
|
+
|
|
9
|
+
const Select = SelectPrimitive.Root
|
|
10
|
+
|
|
11
|
+
function SelectContent({
|
|
12
|
+
align = "center",
|
|
13
|
+
alignItemWithTrigger = true,
|
|
14
|
+
alignOffset = 0,
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
side = "bottom",
|
|
18
|
+
sideOffset = 4,
|
|
19
|
+
...props
|
|
20
|
+
}: Pick<
|
|
21
|
+
SelectPrimitive.Positioner.Props,
|
|
22
|
+
"align" | "alignItemWithTrigger" | "alignOffset" | "side" | "sideOffset"
|
|
23
|
+
> &
|
|
24
|
+
SelectPrimitive.Popup.Props) {
|
|
25
|
+
return (
|
|
26
|
+
<SelectPrimitive.Portal>
|
|
27
|
+
<SelectPrimitive.Positioner
|
|
28
|
+
align={align}
|
|
29
|
+
alignItemWithTrigger={alignItemWithTrigger}
|
|
30
|
+
alignOffset={alignOffset}
|
|
31
|
+
className="isolate z-50"
|
|
32
|
+
side={side}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
>
|
|
35
|
+
<SelectPrimitive.Popup
|
|
36
|
+
className={cn(
|
|
37
|
+
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-md shadow-md ring-1 duration-100 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
data-slot="select-content"
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<SelectScrollUpButton />
|
|
44
|
+
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
|
45
|
+
<SelectScrollDownButton />
|
|
46
|
+
</SelectPrimitive.Popup>
|
|
47
|
+
</SelectPrimitive.Positioner>
|
|
48
|
+
</SelectPrimitive.Portal>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
|
53
|
+
return (
|
|
54
|
+
<SelectPrimitive.Group
|
|
55
|
+
className={cn("scroll-my-1 p-1", className)}
|
|
56
|
+
data-slot="select-group"
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function SelectItem({ children, className, ...props }: SelectPrimitive.Item.Props) {
|
|
63
|
+
return (
|
|
64
|
+
<SelectPrimitive.Item
|
|
65
|
+
className={cn(
|
|
66
|
+
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
data-slot="select-item"
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
<SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
|
73
|
+
{children}
|
|
74
|
+
</SelectPrimitive.ItemText>
|
|
75
|
+
<SelectPrimitive.ItemIndicator
|
|
76
|
+
render={
|
|
77
|
+
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<CheckIcon className="pointer-events-none" />
|
|
81
|
+
</SelectPrimitive.ItemIndicator>
|
|
82
|
+
</SelectPrimitive.Item>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
|
|
87
|
+
return (
|
|
88
|
+
<SelectPrimitive.GroupLabel
|
|
89
|
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
90
|
+
data-slot="select-label"
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function SelectScrollDownButton({
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
|
100
|
+
return (
|
|
101
|
+
<SelectPrimitive.ScrollDownArrow
|
|
102
|
+
className={cn(
|
|
103
|
+
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full",
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
data-slot="select-scroll-down-button"
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<ChevronDownIcon />
|
|
110
|
+
</SelectPrimitive.ScrollDownArrow>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function SelectScrollUpButton({
|
|
115
|
+
className,
|
|
116
|
+
...props
|
|
117
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
|
118
|
+
return (
|
|
119
|
+
<SelectPrimitive.ScrollUpArrow
|
|
120
|
+
className={cn(
|
|
121
|
+
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full",
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
data-slot="select-scroll-up-button"
|
|
125
|
+
{...props}
|
|
126
|
+
>
|
|
127
|
+
<ChevronUpIcon />
|
|
128
|
+
</SelectPrimitive.ScrollUpArrow>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
|
|
133
|
+
return (
|
|
134
|
+
<SelectPrimitive.Separator
|
|
135
|
+
className={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)}
|
|
136
|
+
data-slot="select-separator"
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function SelectTrigger({
|
|
143
|
+
children,
|
|
144
|
+
className,
|
|
145
|
+
size = "default",
|
|
146
|
+
...props
|
|
147
|
+
}: SelectPrimitive.Trigger.Props & {
|
|
148
|
+
size?: "default" | "sm"
|
|
149
|
+
}) {
|
|
150
|
+
return (
|
|
151
|
+
<SelectPrimitive.Trigger
|
|
152
|
+
className={cn(
|
|
153
|
+
"border-input data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 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:aria-invalid:border-destructive/50 gap-1.5 rounded-md border bg-transparent py-2 pr-2 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
data-size={size}
|
|
157
|
+
data-slot="select-trigger"
|
|
158
|
+
{...props}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
<SelectPrimitive.Icon
|
|
162
|
+
render={<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />}
|
|
163
|
+
/>
|
|
164
|
+
</SelectPrimitive.Trigger>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
|
169
|
+
return (
|
|
170
|
+
<SelectPrimitive.Value
|
|
171
|
+
className={cn("flex flex-1 text-left", className)}
|
|
172
|
+
data-slot="select-value"
|
|
173
|
+
{...props}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
Select,
|
|
180
|
+
SelectContent,
|
|
181
|
+
SelectGroup,
|
|
182
|
+
SelectItem,
|
|
183
|
+
SelectLabel,
|
|
184
|
+
SelectScrollDownButton,
|
|
185
|
+
SelectScrollUpButton,
|
|
186
|
+
SelectSeparator,
|
|
187
|
+
SelectTrigger,
|
|
188
|
+
SelectValue,
|
|
189
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
|
4
|
+
|
|
5
|
+
import { cn } from "~/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
|
|
8
|
+
return (
|
|
9
|
+
<SeparatorPrimitive
|
|
10
|
+
className={cn(
|
|
11
|
+
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
orientation={orientation}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Separator }
|
|
@@ -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
|
+
className={cn(
|
|
9
|
+
"border-input dark:bg-input/30 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:aria-invalid:border-destructive/50 rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
data-slot="textarea"
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Moon, Sun } from "lucide-react"
|
|
4
|
+
import { useTheme } from "next-themes"
|
|
5
|
+
import { type ComponentPropsWithRef } from "react"
|
|
6
|
+
|
|
7
|
+
import { Button } from "~/components/ui/button"
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuCheckboxItem,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "~/components/ui/dropdown-menu"
|
|
14
|
+
|
|
15
|
+
export function ThemeToggle({ ...divProps }: ComponentPropsWithRef<"div">) {
|
|
16
|
+
const { resolvedTheme, setTheme, theme } = useTheme()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div {...divProps}>
|
|
20
|
+
<DropdownMenu>
|
|
21
|
+
<DropdownMenuTrigger
|
|
22
|
+
render={<Button size="icon" variant="outline" />}
|
|
23
|
+
suppressHydrationWarning
|
|
24
|
+
>
|
|
25
|
+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
26
|
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
27
|
+
<span className="sr-only">Toggle theme</span>
|
|
28
|
+
</DropdownMenuTrigger>
|
|
29
|
+
<DropdownMenuContent>
|
|
30
|
+
<DropdownMenuCheckboxItem
|
|
31
|
+
checked={Boolean(theme && theme === "light")}
|
|
32
|
+
disabled={Boolean(theme && theme === "light")}
|
|
33
|
+
onCheckedChange={(val) => {
|
|
34
|
+
if (val) {
|
|
35
|
+
setTheme("light")
|
|
36
|
+
}
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
Light
|
|
40
|
+
</DropdownMenuCheckboxItem>
|
|
41
|
+
<DropdownMenuCheckboxItem
|
|
42
|
+
checked={Boolean(theme && theme === "dark")}
|
|
43
|
+
disabled={Boolean(theme && theme === "dark")}
|
|
44
|
+
onCheckedChange={(val) => {
|
|
45
|
+
if (val) {
|
|
46
|
+
setTheme("dark")
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
Dark
|
|
51
|
+
</DropdownMenuCheckboxItem>
|
|
52
|
+
<DropdownMenuCheckboxItem
|
|
53
|
+
checked={Boolean(theme && theme === "system")}
|
|
54
|
+
disabled={Boolean(theme && theme === "system")}
|
|
55
|
+
onCheckedChange={(val) => {
|
|
56
|
+
if (val) {
|
|
57
|
+
setTheme("system")
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
System
|
|
62
|
+
</DropdownMenuCheckboxItem>
|
|
63
|
+
</DropdownMenuContent>
|
|
64
|
+
</DropdownMenu>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export * from "./auth"
|
|
2
|
+
|
|
3
|
+
// Better Auth
|
|
4
|
+
export const TABLE_SLUG_USERS = "user" as const
|
|
5
|
+
export const TABLE_SLUG_ACCOUNTS = "account" as const
|
|
6
|
+
export const TABLE_SLUG_SESSIONS = "session" as const
|
|
7
|
+
export const TABLE_SLUG_VERIFICATIONS = "verification" as const
|
|
8
|
+
export const TABLE_SLUG_JWKS = "jwks" as const
|
|
9
|
+
|
|
10
|
+
export const TABLE_SLUG_MEDIA = "media" as const
|
|
11
|
+
|
|
12
|
+
export const AUTH_PROVIDERS = {
|
|
13
|
+
apple: "apple",
|
|
14
|
+
atlassian: "atlassian",
|
|
15
|
+
cognito: "cognito",
|
|
16
|
+
discord: "discord",
|
|
17
|
+
dropbox: "dropbox",
|
|
18
|
+
facebook: "facebook",
|
|
19
|
+
figma: "figma",
|
|
20
|
+
github: "github",
|
|
21
|
+
gitlab: "gitlab",
|
|
22
|
+
google: "google",
|
|
23
|
+
huggingface: "huggingface",
|
|
24
|
+
kakao: "kakao",
|
|
25
|
+
kick: "kick",
|
|
26
|
+
line: "line",
|
|
27
|
+
linear: "linear",
|
|
28
|
+
linkedin: "linkedin",
|
|
29
|
+
microsoft: "microsoft",
|
|
30
|
+
naver: "naver",
|
|
31
|
+
notion: "notion",
|
|
32
|
+
paypal: "paypal",
|
|
33
|
+
reddit: "reddit",
|
|
34
|
+
roblox: "roblox",
|
|
35
|
+
salesforce: "salesforce",
|
|
36
|
+
slack: "slack",
|
|
37
|
+
spotify: "spotify",
|
|
38
|
+
tiktok: "tiktok",
|
|
39
|
+
twitch: "twitch",
|
|
40
|
+
twitter: "twitter",
|
|
41
|
+
vk: "vk",
|
|
42
|
+
zoom: "zoom",
|
|
43
|
+
} as const
|
|
44
|
+
export type AuthProvider = (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-nextjs"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
/**
|
|
6
|
+
* Specify your client-side environment variables schema here. This way you can ensure the app
|
|
7
|
+
* isn't built with invalid env vars. To expose them to the client, prefix them with
|
|
8
|
+
* `NEXT_PUBLIC_`.
|
|
9
|
+
*/
|
|
10
|
+
client: {
|
|
11
|
+
NEXT_PUBLIC_CONVEX_SITE_URL: z.string(),
|
|
12
|
+
NEXT_PUBLIC_CONVEX_URL: z.string(),
|
|
13
|
+
NEXT_PUBLIC_SITE_URL: z.string(),
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
|
|
18
|
+
* `SOME_VAR=''` will throw an error.
|
|
19
|
+
*/
|
|
20
|
+
emptyStringAsUndefined: true,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
|
24
|
+
* middlewares) or client-side so we need to destruct manually.
|
|
25
|
+
*/
|
|
26
|
+
runtimeEnv: {
|
|
27
|
+
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
|
|
28
|
+
// {{OAUTH_ENV_RUNTIME_MAPPING}}
|
|
29
|
+
NEXT_PUBLIC_CONVEX_SITE_URL: process.env.NEXT_PUBLIC_CONVEX_SITE_URL,
|
|
30
|
+
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
|
|
31
|
+
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
|
|
32
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Specify your server-side environment variables schema here. This way you can ensure the app
|
|
36
|
+
* isn't built with invalid env vars.
|
|
37
|
+
*/
|
|
38
|
+
server: {
|
|
39
|
+
BETTER_AUTH_SECRET: z.string(),
|
|
40
|
+
// {{OAUTH_ENV_SERVER_SCHEMA}}
|
|
41
|
+
NODE_ENV: z.enum(["development", "test", "production"]),
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
|
45
|
+
* useful for Docker builds.
|
|
46
|
+
*/
|
|
47
|
+
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
|
48
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cookies } from "next/headers"
|
|
2
|
+
import { type NextRequest, NextResponse } from "next/server"
|
|
3
|
+
|
|
4
|
+
export async function proxy(request: NextRequest) {
|
|
5
|
+
if (request.nextUrl.pathname === "/") {
|
|
6
|
+
return NextResponse.next()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const cookieStore = await cookies()
|
|
10
|
+
const sessionToken = cookieStore.get("better-auth.session_token")
|
|
11
|
+
|
|
12
|
+
if (!sessionToken) {
|
|
13
|
+
return NextResponse.redirect(new URL("/auth/sign-in", request.url))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return NextResponse.next()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const config = {
|
|
20
|
+
matcher: [
|
|
21
|
+
"/((?!api|_next/static|_next/image|favicon.ico|models/*|staging/*|auth/sign-in|auth/sign-up|$).*)",
|
|
22
|
+
],
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineAccess } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_USERS, USER_ROLES } from "~/db/constants"
|
|
4
|
+
import { users } from "~/vexcms/collections"
|
|
5
|
+
|
|
6
|
+
export const access = defineAccess({
|
|
7
|
+
permissions: {
|
|
8
|
+
admin: {
|
|
9
|
+
// Admins have full access to all resources
|
|
10
|
+
user: true,
|
|
11
|
+
},
|
|
12
|
+
user: {
|
|
13
|
+
[TABLE_SLUG_USERS]: {
|
|
14
|
+
create: false,
|
|
15
|
+
delete: false,
|
|
16
|
+
read: ({ data: targetUser, user }) => {
|
|
17
|
+
return targetUser._id === user._id
|
|
18
|
+
},
|
|
19
|
+
update: ({ data: targetUser, user }) => {
|
|
20
|
+
return targetUser._id === user._id
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
resources: [users],
|
|
26
|
+
roles: [USER_ROLES.user, USER_ROLES.admin],
|
|
27
|
+
userCollection: users,
|
|
28
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Add your collection exports here
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineMediaCollection } from "@vexcms/core"
|
|
2
|
+
|
|
3
|
+
import { TABLE_SLUG_MEDIA } from "~/db/constants"
|
|
4
|
+
|
|
5
|
+
export const media = defineMediaCollection({
|
|
6
|
+
slug: TABLE_SLUG_MEDIA,
|
|
7
|
+
admin: {
|
|
8
|
+
group: "Media",
|
|
9
|
+
useAsTitle: "filename",
|
|
10
|
+
},
|
|
11
|
+
labels: {
|
|
12
|
+
plural: "Media",
|
|
13
|
+
singular: "Media",
|
|
14
|
+
},
|
|
15
|
+
})
|