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.
Files changed (116) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2409 -0
  3. package/package.json +37 -0
  4. package/templates/base-nextjs/.prettierignore +26 -0
  5. package/templates/base-nextjs/.prettierrc +7 -0
  6. package/templates/base-nextjs/README.md +256 -0
  7. package/templates/base-nextjs/_gitignore +2 -0
  8. package/templates/base-nextjs/components.json +24 -0
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
  10. package/templates/base-nextjs/convex/_generated/api.js +23 -0
  11. package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
  12. package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
  13. package/templates/base-nextjs/convex/_generated/server.js +93 -0
  14. package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
  15. package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
  16. package/templates/base-nextjs/convex/auth/api.ts +8 -0
  17. package/templates/base-nextjs/convex/auth/config.ts +10 -0
  18. package/templates/base-nextjs/convex/auth/db.ts +303 -0
  19. package/templates/base-nextjs/convex/auth/index.ts +14 -0
  20. package/templates/base-nextjs/convex/auth/options.ts +63 -0
  21. package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
  22. package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
  23. package/templates/base-nextjs/convex/auth.config.ts +7 -0
  24. package/templates/base-nextjs/convex/convex.config.ts +5 -0
  25. package/templates/base-nextjs/convex/http.ts +28 -0
  26. package/templates/base-nextjs/convex/schema.ts +3 -0
  27. package/templates/base-nextjs/convex/vex/auth.ts +38 -0
  28. package/templates/base-nextjs/convex/vex/collections.ts +321 -0
  29. package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
  30. package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
  31. package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
  32. package/templates/base-nextjs/convex/vex/media.ts +246 -0
  33. package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
  34. package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
  35. package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
  36. package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
  37. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
  38. package/templates/base-nextjs/convex/vex/versions.ts +588 -0
  39. package/templates/base-nextjs/eslint.config.mjs +164 -0
  40. package/templates/base-nextjs/next.config.ts +10 -0
  41. package/templates/base-nextjs/package.json +67 -0
  42. package/templates/base-nextjs/postcss.config.mjs +7 -0
  43. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  44. package/templates/base-nextjs/public/file.svg +1 -0
  45. package/templates/base-nextjs/public/globe.svg +1 -0
  46. package/templates/base-nextjs/public/next.svg +1 -0
  47. package/templates/base-nextjs/public/vercel.svg +1 -0
  48. package/templates/base-nextjs/public/window.svg +1 -0
  49. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
  50. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
  51. package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
  52. package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
  53. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
  54. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
  55. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
  56. package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
  57. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
  58. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
  59. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
  60. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
  61. package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
  62. package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
  63. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  64. package/templates/base-nextjs/src/app/globals.css +130 -0
  65. package/templates/base-nextjs/src/app/layout.tsx +30 -0
  66. package/templates/base-nextjs/src/auth/client.tsx +41 -0
  67. package/templates/base-nextjs/src/auth/permissions.ts +102 -0
  68. package/templates/base-nextjs/src/auth/server.ts +16 -0
  69. package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
  70. package/templates/base-nextjs/src/auth/types.ts +3 -0
  71. package/templates/base-nextjs/src/components/component-example.tsx +474 -0
  72. package/templates/base-nextjs/src/components/example.tsx +52 -0
  73. package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
  74. package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
  75. package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
  76. package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
  77. package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
  78. package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
  79. package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
  80. package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
  81. package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
  82. package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
  83. package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
  84. package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
  85. package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
  86. package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
  87. package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
  88. package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
  89. package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
  90. package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
  91. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
  92. package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
  93. package/templates/base-nextjs/src/db/constants/index.ts +44 -0
  94. package/templates/base-nextjs/src/db/types.ts +6 -0
  95. package/templates/base-nextjs/src/env.mjs +48 -0
  96. package/templates/base-nextjs/src/lib/utils.ts +6 -0
  97. package/templates/base-nextjs/src/proxy.ts +23 -0
  98. package/templates/base-nextjs/src/vexcms/access.ts +28 -0
  99. package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
  100. package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
  101. package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
  102. package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
  103. package/templates/base-nextjs/tsconfig.json +60 -0
  104. package/templates/base-nextjs/vex.config.ts +23 -0
  105. package/templates/marketing-site/convex/pages.ts +46 -0
  106. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
  107. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
  108. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
  109. package/templates/marketing-site/src/db/constants/index.ts +52 -0
  110. package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
  111. package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
  112. package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
  113. package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
  114. package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
  115. package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
  116. 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,8 @@
1
+ export const USER_ROLES = {
2
+ admin: "admin",
3
+ user: "user",
4
+ editor: "editor",
5
+ member: "member",
6
+ author: "author",
7
+ } as const
8
+ export type UserRole = (typeof USER_ROLES)[keyof typeof USER_ROLES]
@@ -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,6 @@
1
+ import { type Doc, type Id } from "@convex/_generated/dataModel"
2
+
3
+ import { type TABLE_SLUG_USERS } from "./constants"
4
+
5
+ export type User = Doc<typeof TABLE_SLUG_USERS>
6
+ export type UserID = Id<typeof TABLE_SLUG_USERS>
@@ -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,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -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,4 @@
1
+ import { betterAuthOptions } from "@convex/auth/options"
2
+ import { vexBetterAuth } from "@vexcms/better-auth"
3
+
4
+ export const auth = vexBetterAuth({ config: betterAuthOptions })
@@ -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
+ })