abdellah0l-stack 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/cli.js +218 -0
- package/package.json +30 -0
- package/template/README.md +61 -0
- package/template/components.json +22 -0
- package/template/drizzle.config.ts +13 -0
- package/template/eslint.config.mjs +25 -0
- package/template/next.config.ts +114 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/src/app/api/v1/auth/[...all]/route.ts +5 -0
- package/template/src/app/api/v1/trpc/[trpc]/route.ts +13 -0
- package/template/src/app/api/v1/uploadthing/core.ts +50 -0
- package/template/src/app/api/v1/uploadthing/route.ts +11 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +121 -0
- package/template/src/app/layout.tsx +50 -0
- package/template/src/app/page.tsx +58 -0
- package/template/src/components/loading-spinner.tsx +18 -0
- package/template/src/components/navigation.tsx +54 -0
- package/template/src/components/query-provider.tsx +27 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +58 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/dialog.tsx +143 -0
- package/template/src/components/ui/input.tsx +21 -0
- package/template/src/components/ui/label.tsx +26 -0
- package/template/src/components/ui/select.tsx +185 -0
- package/template/src/components/ui/tabs.tsx +55 -0
- package/template/src/components/ui/textarea.tsx +23 -0
- package/template/src/data/env/client.ts +7 -0
- package/template/src/data/env/server.ts +13 -0
- package/template/src/drizzle/db.ts +6 -0
- package/template/src/drizzle/schema/app-schema.ts +31 -0
- package/template/src/drizzle/schema/auth-schema.ts +55 -0
- package/template/src/drizzle/schema/index.ts +14 -0
- package/template/src/hooks/use-auth.ts +32 -0
- package/template/src/hooks/use-debounce.ts +18 -0
- package/template/src/lib/arcjet.ts +45 -0
- package/template/src/lib/auth-client.ts +7 -0
- package/template/src/lib/auth.ts +50 -0
- package/template/src/lib/use-mobile.ts +29 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/middleware.ts +14 -0
- package/template/src/server/index.ts +13 -0
- package/template/src/server/routers/posts.ts +93 -0
- package/template/src/server/routers/users.ts +56 -0
- package/template/src/server/trpc.ts +38 -0
- package/template/src/types/index.ts +10 -0
- package/template/src/utils/trpc.ts +5 -0
- package/template/src/utils/uploadthing.ts +10 -0
- package/template/tsconfig.json +42 -0
|
@@ -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 sm:max-w-lg",
|
|
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,26 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const labelVariants = cva(
|
|
10
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const Label = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
16
|
+
VariantProps<typeof labelVariants>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<LabelPrimitive.Root
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(labelVariants(), className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
Label.displayName = LabelPrimitive.Root.displayName
|
|
25
|
+
|
|
26
|
+
export { Label }
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
5
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
function Select({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
12
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SelectGroup({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
18
|
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function SelectValue({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
24
|
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SelectTrigger({
|
|
28
|
+
className,
|
|
29
|
+
size = "default",
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
33
|
+
size?: "sm" | "default"
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<SelectPrimitive.Trigger
|
|
37
|
+
data-slot="select-trigger"
|
|
38
|
+
data-size={size}
|
|
39
|
+
className={cn(
|
|
40
|
+
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
<SelectPrimitive.Icon asChild>
|
|
47
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
48
|
+
</SelectPrimitive.Icon>
|
|
49
|
+
</SelectPrimitive.Trigger>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SelectContent({
|
|
54
|
+
className,
|
|
55
|
+
children,
|
|
56
|
+
position = "popper",
|
|
57
|
+
...props
|
|
58
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
59
|
+
return (
|
|
60
|
+
<SelectPrimitive.Portal>
|
|
61
|
+
<SelectPrimitive.Content
|
|
62
|
+
data-slot="select-content"
|
|
63
|
+
className={cn(
|
|
64
|
+
"bg-popover text-popover-foreground 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 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
65
|
+
position === "popper" &&
|
|
66
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
position={position}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
<SelectScrollUpButton />
|
|
73
|
+
<SelectPrimitive.Viewport
|
|
74
|
+
className={cn(
|
|
75
|
+
"p-1",
|
|
76
|
+
position === "popper" &&
|
|
77
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</SelectPrimitive.Viewport>
|
|
82
|
+
<SelectScrollDownButton />
|
|
83
|
+
</SelectPrimitive.Content>
|
|
84
|
+
</SelectPrimitive.Portal>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function SelectLabel({
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
92
|
+
return (
|
|
93
|
+
<SelectPrimitive.Label
|
|
94
|
+
data-slot="select-label"
|
|
95
|
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function SelectItem({
|
|
102
|
+
className,
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
106
|
+
return (
|
|
107
|
+
<SelectPrimitive.Item
|
|
108
|
+
data-slot="select-item"
|
|
109
|
+
className={cn(
|
|
110
|
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
>
|
|
115
|
+
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
|
116
|
+
<SelectPrimitive.ItemIndicator>
|
|
117
|
+
<CheckIcon className="size-4" />
|
|
118
|
+
</SelectPrimitive.ItemIndicator>
|
|
119
|
+
</span>
|
|
120
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
121
|
+
</SelectPrimitive.Item>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function SelectSeparator({
|
|
126
|
+
className,
|
|
127
|
+
...props
|
|
128
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
129
|
+
return (
|
|
130
|
+
<SelectPrimitive.Separator
|
|
131
|
+
data-slot="select-separator"
|
|
132
|
+
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function SelectScrollUpButton({
|
|
139
|
+
className,
|
|
140
|
+
...props
|
|
141
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
142
|
+
return (
|
|
143
|
+
<SelectPrimitive.ScrollUpButton
|
|
144
|
+
data-slot="select-scroll-up-button"
|
|
145
|
+
className={cn(
|
|
146
|
+
"flex cursor-default items-center justify-center py-1",
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
{...props}
|
|
150
|
+
>
|
|
151
|
+
<ChevronUpIcon className="size-4" />
|
|
152
|
+
</SelectPrimitive.ScrollUpButton>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function SelectScrollDownButton({
|
|
157
|
+
className,
|
|
158
|
+
...props
|
|
159
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
160
|
+
return (
|
|
161
|
+
<SelectPrimitive.ScrollDownButton
|
|
162
|
+
data-slot="select-scroll-down-button"
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex cursor-default items-center justify-center py-1",
|
|
165
|
+
className
|
|
166
|
+
)}
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
<ChevronDownIcon className="size-4" />
|
|
170
|
+
</SelectPrimitive.ScrollDownButton>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
Select,
|
|
176
|
+
SelectContent,
|
|
177
|
+
SelectGroup,
|
|
178
|
+
SelectItem,
|
|
179
|
+
SelectLabel,
|
|
180
|
+
SelectScrollDownButton,
|
|
181
|
+
SelectScrollUpButton,
|
|
182
|
+
SelectSeparator,
|
|
183
|
+
SelectTrigger,
|
|
184
|
+
SelectValue,
|
|
185
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const Tabs = TabsPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const TabsList = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<TabsPrimitive.List
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
TabsList.displayName = TabsPrimitive.List.displayName
|
|
24
|
+
|
|
25
|
+
const TabsTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<TabsPrimitive.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
))
|
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
39
|
+
|
|
40
|
+
const TabsContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<TabsPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
))
|
|
53
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
54
|
+
|
|
55
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
6
|
+
|
|
7
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
8
|
+
({ className, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<textarea
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
ref={ref}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
Textarea.displayName = "Textarea"
|
|
22
|
+
|
|
23
|
+
export { Textarea }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-nextjs";
|
|
2
|
+
|
|
3
|
+
export const env = createEnv({
|
|
4
|
+
client: {}, // client-side environment variables
|
|
5
|
+
emptyStringAsUndefined: true, // treat empty strings as undefined
|
|
6
|
+
experimental__runtimeEnv: {} // no process.env on the client
|
|
7
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createEnv } from "@t3-oss/env-nextjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const env = createEnv({
|
|
5
|
+
server: {
|
|
6
|
+
ARCJET_KEY: z.string().optional(),
|
|
7
|
+
DATABASE_URL: z.string().url(),
|
|
8
|
+
AI_GATEWAY_API_KEY: z.string().min(1),
|
|
9
|
+
}, // server-side environment variables
|
|
10
|
+
emptyStringAsUndefined: true, // treat empty strings as undefined
|
|
11
|
+
experimental__runtimeEnv: process.env, // use process.env at runtime
|
|
12
|
+
skipValidation: false, // do not skip validation
|
|
13
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pgTable,
|
|
3
|
+
uuid,
|
|
4
|
+
varchar,
|
|
5
|
+
text,
|
|
6
|
+
timestamp,
|
|
7
|
+
index,
|
|
8
|
+
} from "drizzle-orm/pg-core";
|
|
9
|
+
import { user } from "./auth-schema";
|
|
10
|
+
|
|
11
|
+
// ========== EXAMPLE: POSTS ==========
|
|
12
|
+
// This is an example table. Replace with your own schema.
|
|
13
|
+
export const posts = pgTable(
|
|
14
|
+
"posts",
|
|
15
|
+
{
|
|
16
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
17
|
+
title: varchar("title", { length: 255 }).notNull(),
|
|
18
|
+
content: text("content").notNull(),
|
|
19
|
+
userId: text("user_id")
|
|
20
|
+
.notNull()
|
|
21
|
+
.references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
22
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
23
|
+
updatedAt: timestamp("updated_at")
|
|
24
|
+
.defaultNow()
|
|
25
|
+
.$onUpdate(() => new Date())
|
|
26
|
+
.notNull(),
|
|
27
|
+
},
|
|
28
|
+
(table) => ({
|
|
29
|
+
userIdx: index("idx_posts_user_id").on(table.userId),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, varchar, boolean } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
// you can ofc customize the schema as you want depending on your auth needs
|
|
4
|
+
export const user = pgTable("user", {
|
|
5
|
+
id: text("id").primaryKey(),
|
|
6
|
+
email: varchar("email", { length: 50 }).notNull().unique(),
|
|
7
|
+
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
8
|
+
name: varchar("name", { length: 50 }).notNull(),
|
|
9
|
+
image: varchar("image", { length: 255 }),
|
|
10
|
+
bio: varchar("bio", { length: 255 }),
|
|
11
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
12
|
+
updatedAt: timestamp("updated_at")
|
|
13
|
+
.defaultNow()
|
|
14
|
+
.$onUpdate(() => new Date())
|
|
15
|
+
.notNull(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// this table is mandatory if you have to deal with sessions and JWTs and tokens and all that
|
|
19
|
+
export const session = pgTable("session", {
|
|
20
|
+
id: text("id").primaryKey(),
|
|
21
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
22
|
+
token: text("token").notNull().unique(),
|
|
23
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
24
|
+
updatedAt: timestamp("updated_at")
|
|
25
|
+
.defaultNow()
|
|
26
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
27
|
+
.notNull(),
|
|
28
|
+
ipAddress: text("ip_address"),
|
|
29
|
+
userAgent: text("user_agent"),
|
|
30
|
+
userId: text("user_id")
|
|
31
|
+
.notNull()
|
|
32
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// delete this table if u don't need OAuth providers
|
|
36
|
+
export const account = pgTable("account", {
|
|
37
|
+
id: text("id").primaryKey(),
|
|
38
|
+
accountId: text("account_id").notNull(),
|
|
39
|
+
providerId: text("provider_id").notNull(),
|
|
40
|
+
userId: text("user_id")
|
|
41
|
+
.notNull()
|
|
42
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
43
|
+
accessToken: text("access_token"),
|
|
44
|
+
refreshToken: text("refresh_token"),
|
|
45
|
+
idToken: text("id_token"),
|
|
46
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
47
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
48
|
+
scope: text("scope"),
|
|
49
|
+
password: text("password"),
|
|
50
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
51
|
+
updatedAt: timestamp("updated_at")
|
|
52
|
+
.defaultNow()
|
|
53
|
+
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
54
|
+
.notNull(),
|
|
55
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// this file aggregates all the database schemas for easier import elsewhere
|
|
4
|
+
import * as app from "./app-schema"
|
|
5
|
+
|
|
6
|
+
import * as auth from "./auth-schema"
|
|
7
|
+
|
|
8
|
+
export const schema = {
|
|
9
|
+
...app,
|
|
10
|
+
...auth,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export * from "./app-schema"
|
|
14
|
+
export * from "./auth-schema"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { authClient } from "@/lib/auth-client";
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
email: string
|
|
9
|
+
image?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AuthState {
|
|
13
|
+
user: User | null
|
|
14
|
+
isLoading: boolean
|
|
15
|
+
isSignedIn: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// a custom hook to access authentication state in your components
|
|
19
|
+
export function useAuth(): AuthState {
|
|
20
|
+
const { data: session, isPending: isLoading } = authClient.useSession();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
user: session?.user ? {
|
|
24
|
+
id: session.user.id,
|
|
25
|
+
name: session.user.name,
|
|
26
|
+
email: session.user.email,
|
|
27
|
+
image: session.user.image || undefined,
|
|
28
|
+
} : null,
|
|
29
|
+
isLoading,
|
|
30
|
+
isSignedIn: !!session,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// a custom hook that debounces a value by a specified delay for better performance and UX
|
|
4
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
5
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handler = setTimeout(() => {
|
|
9
|
+
setDebouncedValue(value)
|
|
10
|
+
}, delay)
|
|
11
|
+
|
|
12
|
+
return () => {
|
|
13
|
+
clearTimeout(handler)
|
|
14
|
+
}
|
|
15
|
+
}, [value, delay])
|
|
16
|
+
|
|
17
|
+
return debouncedValue
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import arcjet, { shield, detectBot, slidingWindow } from "@arcjet/next";
|
|
2
|
+
import { env } from "@/data/env/server";
|
|
3
|
+
|
|
4
|
+
// initialize Arcjet with rules for security and rate limiting
|
|
5
|
+
|
|
6
|
+
const ARCJET_KEY = env.ARCJET_KEY!;
|
|
7
|
+
if (!ARCJET_KEY) {
|
|
8
|
+
throw new Error("ARCJET_KEY environment variable is required");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Arcjet instance to be used in API routes and server-side functions
|
|
12
|
+
export const aj = arcjet({
|
|
13
|
+
key: ARCJET_KEY,
|
|
14
|
+
rules: [
|
|
15
|
+
shield({ mode: "LIVE" }), // the job of this rule is to block malicious requests like SQLi, XSS, etc. (mode: "LIVE" means that it will actually block them)
|
|
16
|
+
detectBot({
|
|
17
|
+
mode: "LIVE", // mode: "LIVE" means that it will actually enforce the rate limit
|
|
18
|
+
allow: ["CATEGORY:SEARCH_ENGINE", "CATEGORY:PREVIEW"], // allow good bots like Googlebot
|
|
19
|
+
}),
|
|
20
|
+
slidingWindow({
|
|
21
|
+
mode: "LIVE", // mode: "LIVE" means that it will actually enforce the rate limit
|
|
22
|
+
interval: "1m", // 1 minute
|
|
23
|
+
max: 50, // max 50 requests per interval
|
|
24
|
+
characteristics: ["userId"], // identify users by their userId
|
|
25
|
+
}), // this rule limits the number of requests to prevent abuse
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// u can create more Arcjet instances with different rules if needed
|
|
30
|
+
// example: a stricter instance for pfp uploads
|
|
31
|
+
export function ProfilePhotoLimiter() {
|
|
32
|
+
return arcjet({
|
|
33
|
+
key: ARCJET_KEY,
|
|
34
|
+
rules: [
|
|
35
|
+
shield({ mode: "LIVE" }),
|
|
36
|
+
slidingWindow({
|
|
37
|
+
mode: "LIVE",
|
|
38
|
+
interval: "7d",
|
|
39
|
+
max: 2,
|
|
40
|
+
characteristics: ["userId"],
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|