@xk2800/nextjs-template 0.7.1-beta.0 → 0.7.1-beta.10
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 +32 -30
- package/components/auth/deviceCheck.tsx +47 -6
- package/components/auth/oneTap.tsx +5 -5
- package/components/dashboard/admin/adminUserConsentCard.tsx +79 -0
- package/components/dashboard/admin/adminUserDevicesCard.tsx +80 -0
- package/components/dashboard/admin/broadcastActions.tsx +148 -0
- package/components/dashboard/admin/confirmSendDialog.tsx +44 -0
- package/components/dashboard/admin/marketingEmailForm.tsx +115 -0
- package/components/dashboard/admin/marketingSection.tsx +67 -0
- package/components/dashboard/admin/subscribersCard.tsx +62 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +33 -0
- package/components/dashboard/admin/systemSettingsSection.tsx +8 -2
- package/components/dashboard/errorFeedback.tsx +2 -4
- package/components/dashboard/feedbackDialog.tsx +4 -4
- package/components/dashboard/marketingUpdatesCard.tsx +62 -0
- package/components/dashboard/marketingUpdatesSection.tsx +10 -0
- package/components/site/cookie-banner.tsx +109 -7
- package/components/ui/alert-dialog.tsx +1 -1
- package/components/ui/badge.tsx +1 -1
- package/components/ui/card.tsx +1 -1
- package/components/ui/data-table.tsx +4 -1
- package/components/ui/dialog.tsx +1 -1
- package/components/ui/dropdown-menu.tsx +2 -2
- package/components/ui/file-upload.tsx +129 -7
- package/components/ui/select.tsx +189 -0
- package/components/ui/switch.tsx +2 -2
- package/dist/activity/logger.js +36 -0
- package/dist/activity/logger.js.map +1 -1
- package/dist/activity/logger.mjs +4 -4
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/activity/queries.js +36 -0
- package/dist/activity/queries.js.map +1 -1
- package/dist/activity/queries.mjs +3 -3
- package/dist/admin/queries.d.mts +19 -1
- package/dist/admin/queries.d.ts +19 -1
- package/dist/admin/queries.js +64 -0
- package/dist/admin/queries.js.map +1 -1
- package/dist/admin/queries.mjs +26 -5
- package/dist/admin/queries.mjs.map +1 -1
- package/dist/api/feedback.js +53 -2
- package/dist/api/feedback.js.map +1 -1
- package/dist/api/feedback.mjs +9 -8
- package/dist/api/feedback.mjs.map +1 -1
- package/dist/api/marketing.d.mts +36 -0
- package/dist/api/marketing.d.ts +36 -0
- package/dist/api/marketing.js +1357 -0
- package/dist/api/marketing.js.map +1 -0
- package/dist/api/marketing.mjs +170 -0
- package/dist/api/marketing.mjs.map +1 -0
- package/dist/auth/helpers.js +51 -0
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +9 -64
- package/dist/auth/helpers.mjs.map +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +51 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +8 -7
- package/dist/{chunk-KAEA35OC.mjs → chunk-6MDQRT6U.mjs} +4 -4
- package/dist/{chunk-KAEA35OC.mjs.map → chunk-6MDQRT6U.mjs.map} +1 -1
- package/dist/{chunk-N6ECRCHY.mjs → chunk-6OAHCEDJ.mjs} +4 -3
- package/dist/chunk-6OAHCEDJ.mjs.map +1 -0
- package/dist/chunk-6Z3WUSDJ.mjs +62 -0
- package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
- package/dist/{chunk-VQ5B4MA7.mjs → chunk-7F2POZM6.mjs} +4 -4
- package/dist/{chunk-VQ5B4MA7.mjs.map → chunk-7F2POZM6.mjs.map} +1 -1
- package/dist/{chunk-XWVAJXAG.mjs → chunk-7QVIOQ47.mjs} +23 -19
- package/dist/chunk-7QVIOQ47.mjs.map +1 -0
- package/dist/chunk-C6RGN6QA.mjs +60 -0
- package/dist/chunk-C6RGN6QA.mjs.map +1 -0
- package/dist/chunk-OVKVD2HH.mjs +25 -0
- package/dist/chunk-OVKVD2HH.mjs.map +1 -0
- package/dist/{chunk-3MGEAGHU.mjs → chunk-P2G23JMY.mjs} +11 -6
- package/dist/chunk-P2G23JMY.mjs.map +1 -0
- package/dist/{chunk-TMJYAOC2.mjs → chunk-TKWNEKK6.mjs} +8 -2
- package/dist/chunk-TKWNEKK6.mjs.map +1 -0
- package/dist/{chunk-ZQCALTKB.mjs → chunk-TR5XFBTD.mjs} +34 -4
- package/dist/chunk-TR5XFBTD.mjs.map +1 -0
- package/dist/{chunk-WZDAMCUX.mjs → chunk-ZJMFE5B2.mjs} +4 -4
- package/dist/{chunk-WZDAMCUX.mjs.map → chunk-ZJMFE5B2.mjs.map} +1 -1
- package/dist/config/env.d.mts +2 -0
- package/dist/config/env.d.ts +2 -0
- package/dist/config/env.js +6 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/env.mjs +1 -1
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +36 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/index.mjs +3 -3
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +30 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/schema.mjs +1 -1
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -1
- package/dist/errors.mjs +4 -4
- package/dist/marketing/index.d.mts +35 -0
- package/dist/marketing/index.d.ts +35 -0
- package/dist/marketing/index.js +125 -0
- package/dist/marketing/index.js.map +1 -0
- package/dist/marketing/index.mjs +6 -0
- package/dist/marketing/index.mjs.map +1 -0
- package/dist/{schema-BNSBWNjK.d.mts → schema-CN_Y6EXv.d.mts} +149 -3
- package/dist/{schema-BNSBWNjK.d.ts → schema-CN_Y6EXv.d.ts} +149 -3
- package/dist/sessions/queries.js +36 -0
- package/dist/sessions/queries.js.map +1 -1
- package/dist/sessions/queries.mjs +3 -3
- package/dist/settings/queries.d.mts +4 -3
- package/dist/settings/queries.d.ts +4 -3
- package/dist/settings/queries.js +42 -0
- package/dist/settings/queries.js.map +1 -1
- package/dist/settings/queries.mjs +4 -4
- package/dist/users/queries.js +36 -0
- package/dist/users/queries.js.map +1 -1
- package/dist/users/queries.mjs +3 -3
- package/lib/cookie-consent.ts +72 -20
- package/lib/cookie-names.ts +47 -0
- package/lib/device-fingerprint.ts +66 -10
- package/lib/recalculate-columns.ts +2 -0
- package/package.json +28 -3
- package/styles/theme.css +17 -3
- package/dist/chunk-3MGEAGHU.mjs.map +0 -1
- package/dist/chunk-N6ECRCHY.mjs.map +0 -1
- package/dist/chunk-TMJYAOC2.mjs.map +0 -1
- package/dist/chunk-XWVAJXAG.mjs.map +0 -1
- package/dist/chunk-ZQCALTKB.mjs.map +0 -1
|
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
47
47
|
<DropdownMenuPrimitive.SubContent
|
|
48
48
|
ref={ref}
|
|
49
49
|
className={cn(
|
|
50
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-
|
|
50
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border-(length:--surface-border-width) border-(--surface-border-color) bg-popover p-1 text-popover-foreground shadow-(--popover-shadow) 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",
|
|
51
51
|
className
|
|
52
52
|
)}
|
|
53
53
|
{...props}
|
|
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
65
65
|
ref={ref}
|
|
66
66
|
sideOffset={sideOffset}
|
|
67
67
|
className={cn(
|
|
68
|
-
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-
|
|
68
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border-(length:--surface-border-width) border-(--surface-border-color) bg-popover p-1 text-popover-foreground shadow-(--popover-shadow) 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",
|
|
69
69
|
className
|
|
70
70
|
)}
|
|
71
71
|
{...props}
|
|
@@ -4,6 +4,8 @@ import * as React from "react"
|
|
|
4
4
|
import { Camera, ImagePlus, Upload, X } from "lucide-react"
|
|
5
5
|
|
|
6
6
|
import { Button } from "./button"
|
|
7
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./dialog"
|
|
8
|
+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet"
|
|
7
9
|
import { cn } from "../../lib/utils"
|
|
8
10
|
|
|
9
11
|
type FileUploadProps = {
|
|
@@ -13,6 +15,13 @@ type FileUploadProps = {
|
|
|
13
15
|
gallery?: boolean
|
|
14
16
|
/** Drop files onto the component. */
|
|
15
17
|
dragDrop?: boolean
|
|
18
|
+
/**
|
|
19
|
+
* Pressing the drop zone opens the picker instead of showing separate buttons. With both
|
|
20
|
+
* `camera` and `gallery` the user is asked which one; with one, it opens directly. Needs `dragDrop`.
|
|
21
|
+
*/
|
|
22
|
+
clickable?: boolean
|
|
23
|
+
/** How a touch-screen user is asked "camera or gallery" with `clickable`: a bottom sheet or a centered modal. */
|
|
24
|
+
chooser?: "sheet" | "modal"
|
|
16
25
|
/** Same format as `<input accept>`. Also enforced on dropped files. */
|
|
17
26
|
accept?: string
|
|
18
27
|
multiple?: boolean
|
|
@@ -23,6 +32,14 @@ type FileUploadProps = {
|
|
|
23
32
|
icon?: React.ReactNode
|
|
24
33
|
/** Drop zone text. Defaults to "Drag and drop files here" (or "a file" when `multiple` is false). */
|
|
25
34
|
label?: React.ReactNode
|
|
35
|
+
/** Smaller sub text under the drop zone label. */
|
|
36
|
+
description?: React.ReactNode
|
|
37
|
+
/** Extra classes for the dashed drop zone box. */
|
|
38
|
+
dropzoneClassName?: string
|
|
39
|
+
/** Extra classes for the label text. */
|
|
40
|
+
labelClassName?: string
|
|
41
|
+
/** Extra classes for the description text. */
|
|
42
|
+
descriptionClassName?: string
|
|
26
43
|
onFilesChange?: (files: File[]) => void
|
|
27
44
|
className?: string
|
|
28
45
|
}
|
|
@@ -41,6 +58,8 @@ function FileUpload({
|
|
|
41
58
|
camera = false,
|
|
42
59
|
gallery = false,
|
|
43
60
|
dragDrop = false,
|
|
61
|
+
clickable = false,
|
|
62
|
+
chooser = "sheet",
|
|
44
63
|
accept = "image/*",
|
|
45
64
|
multiple = true,
|
|
46
65
|
maxFiles,
|
|
@@ -48,6 +67,10 @@ function FileUpload({
|
|
|
48
67
|
disabled = false,
|
|
49
68
|
icon = <Upload className="size-6" />,
|
|
50
69
|
label,
|
|
70
|
+
description,
|
|
71
|
+
dropzoneClassName,
|
|
72
|
+
labelClassName,
|
|
73
|
+
descriptionClassName,
|
|
51
74
|
onFilesChange,
|
|
52
75
|
className,
|
|
53
76
|
}: FileUploadProps) {
|
|
@@ -56,12 +79,18 @@ function FileUpload({
|
|
|
56
79
|
const [dragging, setDragging] = React.useState(false)
|
|
57
80
|
const cameraRef = React.useRef<HTMLInputElement>(null)
|
|
58
81
|
const galleryRef = React.useRef<HTMLInputElement>(null)
|
|
82
|
+
const [askOpen, setAskOpen] = React.useState(false)
|
|
83
|
+
const pointerType = React.useRef("")
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
85
|
+
// Created and revoked in the same effect: with useMemo, Strict Mode's
|
|
86
|
+
// simulated unmount revoked the URLs and the remount reused the dead ones.
|
|
87
|
+
const [previews, setPreviews] = React.useState<(string | null)[]>([])
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
const urls = files.map((f) => (f.type.startsWith("image/") ? URL.createObjectURL(f) : null))
|
|
90
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- object URLs must be created where they're revoked
|
|
91
|
+
setPreviews(urls)
|
|
92
|
+
return () => urls.forEach((url) => url && URL.revokeObjectURL(url))
|
|
93
|
+
}, [files])
|
|
65
94
|
|
|
66
95
|
const update = (next: File[]) => {
|
|
67
96
|
setFiles(next)
|
|
@@ -96,6 +125,37 @@ function FileUpload({
|
|
|
96
125
|
|
|
97
126
|
if (!camera && !gallery && !dragDrop) return null
|
|
98
127
|
|
|
128
|
+
// Pressing the drop zone only does something if there's a picker to open.
|
|
129
|
+
const pickable = clickable && !disabled && (camera || gallery)
|
|
130
|
+
// The camera only means something on touch devices, so mouse and keyboard skip the
|
|
131
|
+
// question and open the gallery (or the only source there is).
|
|
132
|
+
const openDefault = () => (gallery ? galleryRef : cameraRef).current?.click()
|
|
133
|
+
const units = multiple ? "files" : "a file"
|
|
134
|
+
|
|
135
|
+
const askTitle = `Add ${multiple ? "photos" : "a photo"}`
|
|
136
|
+
const askDescription = "Take a new photo or pick from your gallery."
|
|
137
|
+
const askButtons = (
|
|
138
|
+
<div className="grid gap-2">
|
|
139
|
+
{[
|
|
140
|
+
{ ref: cameraRef, Icon: Camera, text: "Take photo" },
|
|
141
|
+
{ ref: galleryRef, Icon: ImagePlus, text: "Choose from gallery" },
|
|
142
|
+
].map(({ ref, Icon, text }) => (
|
|
143
|
+
<Button
|
|
144
|
+
key={text}
|
|
145
|
+
type="button"
|
|
146
|
+
variant="outline"
|
|
147
|
+
className="h-12 justify-start"
|
|
148
|
+
onClick={() => {
|
|
149
|
+
setAskOpen(false)
|
|
150
|
+
ref.current?.click()
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<Icon /> {text}
|
|
154
|
+
</Button>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
|
|
99
159
|
const buttons = (
|
|
100
160
|
<div className="flex flex-wrap justify-center gap-2">
|
|
101
161
|
{camera && (
|
|
@@ -122,6 +182,28 @@ function FileUpload({
|
|
|
122
182
|
|
|
123
183
|
{dragDrop ? (
|
|
124
184
|
<div
|
|
185
|
+
role={pickable ? "button" : undefined}
|
|
186
|
+
tabIndex={pickable ? 0 : undefined}
|
|
187
|
+
onPointerDown={pickable ? (e) => (pointerType.current = e.pointerType) : undefined}
|
|
188
|
+
onClick={
|
|
189
|
+
pickable
|
|
190
|
+
? () => {
|
|
191
|
+
if (camera && gallery && pointerType.current !== "mouse") setAskOpen(true)
|
|
192
|
+
else openDefault()
|
|
193
|
+
pointerType.current = ""
|
|
194
|
+
}
|
|
195
|
+
: undefined
|
|
196
|
+
}
|
|
197
|
+
onKeyDown={
|
|
198
|
+
pickable
|
|
199
|
+
? (e) => {
|
|
200
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
openDefault()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
: undefined
|
|
206
|
+
}
|
|
125
207
|
onDragOver={(e) => {
|
|
126
208
|
e.preventDefault()
|
|
127
209
|
if (!disabled) setDragging(true)
|
|
@@ -135,18 +217,58 @@ function FileUpload({
|
|
|
135
217
|
aria-disabled={disabled}
|
|
136
218
|
className={cn(
|
|
137
219
|
"flex flex-col items-center gap-3 rounded-lg border-2 border-dashed p-6 text-center text-sm text-muted-foreground transition-colors",
|
|
220
|
+
pickable && "cursor-pointer outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring",
|
|
221
|
+
// Before the state classes so a custom border can't hide the drag highlight.
|
|
222
|
+
dropzoneClassName,
|
|
138
223
|
dragging && "border-primary bg-primary/5",
|
|
139
224
|
disabled && "opacity-50"
|
|
140
225
|
)}
|
|
141
226
|
>
|
|
142
227
|
{icon}
|
|
143
|
-
<
|
|
144
|
-
|
|
228
|
+
<div className="space-y-1">
|
|
229
|
+
<p className={labelClassName}>
|
|
230
|
+
{label ??
|
|
231
|
+
(pickable ? (
|
|
232
|
+
<>
|
|
233
|
+
<span className="pointer-coarse:hidden">Drag and drop {units} here, or click to browse</span>
|
|
234
|
+
<span className="hidden pointer-coarse:inline">Tap to add {units}</span>
|
|
235
|
+
</>
|
|
236
|
+
) : (
|
|
237
|
+
`Drag and drop ${units} here`
|
|
238
|
+
))}
|
|
239
|
+
</p>
|
|
240
|
+
{description && <p className={cn("text-xs", descriptionClassName)}>{description}</p>}
|
|
241
|
+
</div>
|
|
242
|
+
{!pickable && buttons}
|
|
145
243
|
</div>
|
|
146
244
|
) : (
|
|
147
245
|
buttons
|
|
148
246
|
)}
|
|
149
247
|
|
|
248
|
+
{pickable && camera && gallery && (
|
|
249
|
+
chooser === "modal" ? (
|
|
250
|
+
<Dialog open={askOpen} onOpenChange={setAskOpen}>
|
|
251
|
+
<DialogContent className="sm:max-w-sm">
|
|
252
|
+
<DialogHeader>
|
|
253
|
+
<DialogTitle>{askTitle}</DialogTitle>
|
|
254
|
+
<DialogDescription>{askDescription}</DialogDescription>
|
|
255
|
+
</DialogHeader>
|
|
256
|
+
{askButtons}
|
|
257
|
+
</DialogContent>
|
|
258
|
+
</Dialog>
|
|
259
|
+
) : (
|
|
260
|
+
<Sheet open={askOpen} onOpenChange={setAskOpen}>
|
|
261
|
+
<SheetContent side="bottom" className="mx-auto max-w-lg rounded-t-xl pb-[max(1rem,env(safe-area-inset-bottom))]">
|
|
262
|
+
<SheetHeader className="pb-0">
|
|
263
|
+
<SheetTitle>{askTitle}</SheetTitle>
|
|
264
|
+
<SheetDescription>{askDescription}</SheetDescription>
|
|
265
|
+
</SheetHeader>
|
|
266
|
+
<div className="px-4">{askButtons}</div>
|
|
267
|
+
</SheetContent>
|
|
268
|
+
</Sheet>
|
|
269
|
+
)
|
|
270
|
+
)}
|
|
271
|
+
|
|
150
272
|
{error && (
|
|
151
273
|
<p role="alert" className="text-sm text-destructive">
|
|
152
274
|
{error}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
6
|
+
import { Select as SelectPrimitive } from "radix-ui"
|
|
7
|
+
|
|
8
|
+
function Select({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
11
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function SelectGroup({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
17
|
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function SelectValue({
|
|
21
|
+
...props
|
|
22
|
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
23
|
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SelectTrigger({
|
|
27
|
+
className,
|
|
28
|
+
size = "default",
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
32
|
+
size?: "sm" | "default"
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<SelectPrimitive.Trigger
|
|
36
|
+
data-slot="select-trigger"
|
|
37
|
+
data-size={size}
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground 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 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<SelectPrimitive.Icon asChild>
|
|
46
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
47
|
+
</SelectPrimitive.Icon>
|
|
48
|
+
</SelectPrimitive.Trigger>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SelectContent({
|
|
53
|
+
className,
|
|
54
|
+
children,
|
|
55
|
+
position = "item-aligned",
|
|
56
|
+
align = "center",
|
|
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
|
+
"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-(length:--surface-border-width) border-(--surface-border-color) bg-popover text-popover-foreground shadow-(--popover-shadow) 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
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
|
+
align={align}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<SelectScrollUpButton />
|
|
74
|
+
<SelectPrimitive.Viewport
|
|
75
|
+
className={cn(
|
|
76
|
+
"p-1",
|
|
77
|
+
position === "popper" &&
|
|
78
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</SelectPrimitive.Viewport>
|
|
83
|
+
<SelectScrollDownButton />
|
|
84
|
+
</SelectPrimitive.Content>
|
|
85
|
+
</SelectPrimitive.Portal>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function SelectLabel({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
93
|
+
return (
|
|
94
|
+
<SelectPrimitive.Label
|
|
95
|
+
data-slot="select-label"
|
|
96
|
+
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function SelectItem({
|
|
103
|
+
className,
|
|
104
|
+
children,
|
|
105
|
+
...props
|
|
106
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
107
|
+
return (
|
|
108
|
+
<SelectPrimitive.Item
|
|
109
|
+
data-slot="select-item"
|
|
110
|
+
className={cn(
|
|
111
|
+
"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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<span
|
|
117
|
+
data-slot="select-item-indicator"
|
|
118
|
+
className="absolute right-2 flex size-3.5 items-center justify-center"
|
|
119
|
+
>
|
|
120
|
+
<SelectPrimitive.ItemIndicator>
|
|
121
|
+
<CheckIcon className="size-4" />
|
|
122
|
+
</SelectPrimitive.ItemIndicator>
|
|
123
|
+
</span>
|
|
124
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
125
|
+
</SelectPrimitive.Item>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function SelectSeparator({
|
|
130
|
+
className,
|
|
131
|
+
...props
|
|
132
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
133
|
+
return (
|
|
134
|
+
<SelectPrimitive.Separator
|
|
135
|
+
data-slot="select-separator"
|
|
136
|
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function SelectScrollUpButton({
|
|
143
|
+
className,
|
|
144
|
+
...props
|
|
145
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
146
|
+
return (
|
|
147
|
+
<SelectPrimitive.ScrollUpButton
|
|
148
|
+
data-slot="select-scroll-up-button"
|
|
149
|
+
className={cn(
|
|
150
|
+
"flex cursor-default items-center justify-center py-1",
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<ChevronUpIcon className="size-4" />
|
|
156
|
+
</SelectPrimitive.ScrollUpButton>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function SelectScrollDownButton({
|
|
161
|
+
className,
|
|
162
|
+
...props
|
|
163
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
164
|
+
return (
|
|
165
|
+
<SelectPrimitive.ScrollDownButton
|
|
166
|
+
data-slot="select-scroll-down-button"
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex cursor-default items-center justify-center py-1",
|
|
169
|
+
className
|
|
170
|
+
)}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<ChevronDownIcon className="size-4" />
|
|
174
|
+
</SelectPrimitive.ScrollDownButton>
|
|
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
|
+
}
|
package/components/ui/switch.tsx
CHANGED
|
@@ -17,7 +17,7 @@ function Switch({
|
|
|
17
17
|
data-slot="switch"
|
|
18
18
|
data-size={size}
|
|
19
19
|
className={cn(
|
|
20
|
-
"peer group/switch inline-flex shrink-0 items-center rounded-
|
|
20
|
+
"peer group/switch inline-flex shrink-0 items-center rounded-(--pill-radius) border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
|
21
21
|
className
|
|
22
22
|
)}
|
|
23
23
|
{...props}
|
|
@@ -25,7 +25,7 @@ function Switch({
|
|
|
25
25
|
<SwitchPrimitive.Thumb
|
|
26
26
|
data-slot="switch-thumb"
|
|
27
27
|
className={cn(
|
|
28
|
-
"pointer-events-none block rounded-
|
|
28
|
+
"pointer-events-none block rounded-(--pill-radius) bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
|
|
29
29
|
)}
|
|
30
30
|
/>
|
|
31
31
|
</SwitchPrimitive.Root>
|
package/dist/activity/logger.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(schema_exports, {
|
|
|
24
24
|
accounts: () => accounts,
|
|
25
25
|
activityLogs: () => activityLogs,
|
|
26
26
|
authThrottle: () => authThrottle,
|
|
27
|
+
consentLog: () => consentLog,
|
|
27
28
|
deviceFingerprints: () => deviceFingerprints,
|
|
28
29
|
errorRefs: () => errorRefs,
|
|
29
30
|
passkeys: () => passkeys,
|
|
@@ -178,10 +179,32 @@ var deviceFingerprints = pgCore.pgTable("device_fingerprint", {
|
|
|
178
179
|
userAgent: pgCore.text("userAgent"),
|
|
179
180
|
country: pgCore.text("country"),
|
|
180
181
|
city: pgCore.text("city"),
|
|
182
|
+
// Latest FingerprintJS signals for this device (lib/device-fingerprint.ts
|
|
183
|
+
// getDeviceSignals(): accessibility signals dropped, large values hashed),
|
|
184
|
+
// shown on the admin user page. Null for rows from before this column.
|
|
185
|
+
components: pgCore.jsonb("components").$type(),
|
|
181
186
|
createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
|
|
182
187
|
}, (table) => [
|
|
183
188
|
pgCore.unique("device_fingerprint_user_visitor").on(table.userId, table.visitorId)
|
|
184
189
|
]);
|
|
190
|
+
var consentLog = pgCore.pgTable("consent_log", {
|
|
191
|
+
id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
|
|
192
|
+
userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
193
|
+
// The cookie_consent value: "all", "necessary" or a comma list of purposes.
|
|
194
|
+
choice: pgCore.text("choice").notNull(),
|
|
195
|
+
// CONSENT_VERSION (lib/cookie-names.ts) at the time — which banner/policy
|
|
196
|
+
// wording the user agreed to.
|
|
197
|
+
version: pgCore.integer("version").notNull(),
|
|
198
|
+
// id of the user's record this one follows ("" for their first). Unique per
|
|
199
|
+
// user, so concurrent posts from several tabs that all saw the same latest
|
|
200
|
+
// record can't each insert a duplicate — one wins, the rest conflict. Works
|
|
201
|
+
// without transactions (neon-http has none). Null on rows from before it.
|
|
202
|
+
previousId: pgCore.text("previousId"),
|
|
203
|
+
createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
|
|
204
|
+
}, (table) => [
|
|
205
|
+
pgCore.index("consent_log_user_idx").on(table.userId, table.createdAt),
|
|
206
|
+
pgCore.unique("consent_log_chain").on(table.userId, table.previousId)
|
|
207
|
+
]);
|
|
185
208
|
var authThrottle = pgCore.pgTable("auth_throttle", {
|
|
186
209
|
fingerprint: pgCore.text("fingerprint").notNull().primaryKey(),
|
|
187
210
|
count: pgCore.integer("count").notNull().default(0),
|
|
@@ -211,6 +234,13 @@ var systemSettings = pgCore.pgTable("system_settings", {
|
|
|
211
234
|
// Attach the error a user hit to feedback they send from it (see
|
|
212
235
|
// lib/feedback-route.ts). Off = only what the user typed is sent.
|
|
213
236
|
feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
|
|
237
|
+
// GDPR/ePrivacy consent mode. On: cookie banner, FingerprintJS and One Tap
|
|
238
|
+
// only after "Accept all", device record erased on withdrawal. Off (no EU/UK
|
|
239
|
+
// users): no banner, treated as consented. Null = follow
|
|
240
|
+
// NEXT_PUBLIC_COOKIE_BANNER (nullable so migrating never flips a deployed
|
|
241
|
+
// banner on or off). proxy.ts mirrors the effective value into the
|
|
242
|
+
// consent_required cookie for the client (lib/cookie-consent.ts).
|
|
243
|
+
consentRequired: pgCore.boolean("consentRequired"),
|
|
214
244
|
updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
|
|
215
245
|
// Who last changed a setting. This row doesn't "belong" to that admin the
|
|
216
246
|
// way sessions/activityLogs belong to their user, so deleting that admin's
|
|
@@ -232,6 +262,12 @@ var envSchema = zod.z.object({
|
|
|
232
262
|
PORT: zod.z.string().default("3000").transform(Number),
|
|
233
263
|
DATABASE_URL: zod.z.string().default(""),
|
|
234
264
|
RESEND_API_KEY: zod.z.string().default(""),
|
|
265
|
+
// Full access key for marketing broadcasts (admin Marketing email page).
|
|
266
|
+
// Blank = use RESEND_API_KEY.
|
|
267
|
+
RESEND_BROADCAST_API_KEY: zod.z.string().default(""),
|
|
268
|
+
// Resend segment users join from the dashboard "Email updates" toggle.
|
|
269
|
+
// Blank = toggle hidden.
|
|
270
|
+
RESEND_MARKETING_SEGMENT_ID: zod.z.string().default(""),
|
|
235
271
|
// Sender for every transactional email. Must be on a domain verified in
|
|
236
272
|
// your Resend account.
|
|
237
273
|
EMAIL_FROM: zod.z.string().default("Acme <onboarding@xavierkhew.xyz>"),
|