@prenta/admin 1.17.0 → 1.17.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/__tests__/lib/seo-service-errors.test.js +2 -6
  3. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
  4. package/dist/lib/seo-service.d.ts +0 -5
  5. package/dist/lib/seo-service.d.ts.map +1 -1
  6. package/dist/lib/seo-service.js +0 -11
  7. package/dist/lib/seo-service.js.map +1 -1
  8. package/dist/prenta-admin.css +1 -1
  9. package/package.json +5 -5
  10. package/src/__tests__/lib/seo-service-errors.test.ts +1 -9
  11. package/src/lib/seo-service.ts +0 -14
  12. package/dist/components/ContentOverviewChart.d.ts +0 -8
  13. package/dist/components/ContentOverviewChart.d.ts.map +0 -1
  14. package/dist/components/ContentOverviewChart.js +0 -20
  15. package/dist/components/ContentOverviewChart.js.map +0 -1
  16. package/dist/components/SEOPerformance.d.ts +0 -5
  17. package/dist/components/SEOPerformance.d.ts.map +0 -1
  18. package/dist/components/SEOPerformance.js +0 -29
  19. package/dist/components/SEOPerformance.js.map +0 -1
  20. package/dist/components/ui/Avatar.d.ts +0 -9
  21. package/dist/components/ui/Avatar.d.ts.map +0 -1
  22. package/dist/components/ui/Avatar.js +0 -21
  23. package/dist/components/ui/Avatar.js.map +0 -1
  24. package/dist/components/ui/CommandPalette.d.ts +0 -14
  25. package/dist/components/ui/CommandPalette.d.ts.map +0 -1
  26. package/dist/components/ui/CommandPalette.js +0 -58
  27. package/dist/components/ui/CommandPalette.js.map +0 -1
  28. package/dist/components/ui/DataTable.d.ts +0 -30
  29. package/dist/components/ui/DataTable.d.ts.map +0 -1
  30. package/dist/components/ui/DataTable.js +0 -44
  31. package/dist/components/ui/DataTable.js.map +0 -1
  32. package/dist/components/ui/Pagination.d.ts +0 -9
  33. package/dist/components/ui/Pagination.d.ts.map +0 -1
  34. package/dist/components/ui/Pagination.js +0 -26
  35. package/dist/components/ui/Pagination.js.map +0 -1
  36. package/dist/components/ui/Select.d.ts +0 -16
  37. package/dist/components/ui/Select.d.ts.map +0 -1
  38. package/dist/components/ui/Select.js +0 -24
  39. package/dist/components/ui/Select.js.map +0 -1
  40. package/dist/components/ui/Toast.d.ts +0 -18
  41. package/dist/components/ui/Toast.d.ts.map +0 -1
  42. package/dist/components/ui/Toast.js +0 -29
  43. package/dist/components/ui/Toast.js.map +0 -1
  44. package/dist/components/ui/index.d.ts +0 -46
  45. package/dist/components/ui/index.d.ts.map +0 -1
  46. package/dist/components/ui/index.js +0 -24
  47. package/dist/components/ui/index.js.map +0 -1
  48. package/dist/hooks/useContentLock.d.ts +0 -12
  49. package/dist/hooks/useContentLock.d.ts.map +0 -1
  50. package/dist/hooks/useContentLock.js +0 -38
  51. package/dist/hooks/useContentLock.js.map +0 -1
  52. package/dist/hooks/useDebounce.d.ts +0 -2
  53. package/dist/hooks/useDebounce.d.ts.map +0 -1
  54. package/dist/hooks/useDebounce.js +0 -11
  55. package/dist/hooks/useDebounce.js.map +0 -1
  56. package/src/components/ContentOverviewChart.tsx +0 -75
  57. package/src/components/SEOPerformance.tsx +0 -163
  58. package/src/components/ui/Avatar.tsx +0 -42
  59. package/src/components/ui/CommandPalette.tsx +0 -133
  60. package/src/components/ui/DataTable.tsx +0 -204
  61. package/src/components/ui/Pagination.tsx +0 -94
  62. package/src/components/ui/Select.tsx +0 -53
  63. package/src/components/ui/Toast.tsx +0 -72
  64. package/src/components/ui/index.ts +0 -61
  65. package/src/hooks/useContentLock.ts +0 -55
  66. package/src/hooks/useDebounce.ts +0 -14
@@ -1,204 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useRef, useEffect } from 'react'
4
-
5
- interface Column {
6
- key: string
7
- label: string
8
- sortable?: boolean
9
- }
10
-
11
- interface Row {
12
- id: string
13
- [key: string]: any
14
- }
15
-
16
- export interface RowAction {
17
- key: string
18
- label: string
19
- icon?: React.ReactNode
20
- destructive?: boolean
21
- onClick: (row: Row) => void
22
- }
23
-
24
- export interface DataTableProps {
25
- columns: Column[]
26
- rows: Row[]
27
- selectedIds: string[]
28
- onSelectionChange: (ids: string[]) => void
29
- sortField?: string
30
- sortDir?: 'asc' | 'desc'
31
- onSort?: (field: string) => void
32
- rowActions?: RowAction[]
33
- onRowClick?: (row: Row) => void
34
- }
35
-
36
- export function DataTable({
37
- columns,
38
- rows,
39
- selectedIds,
40
- onSelectionChange,
41
- sortField,
42
- sortDir,
43
- onSort,
44
- rowActions,
45
- onRowClick,
46
- }: DataTableProps) {
47
- const allSelected = rows.length > 0 && selectedIds.length === rows.length
48
-
49
- function toggleAll() {
50
- onSelectionChange(allSelected ? [] : rows.map((r) => r.id))
51
- }
52
-
53
- function toggleRow(id: string) {
54
- onSelectionChange(
55
- selectedIds.includes(id) ? selectedIds.filter((s) => s !== id) : [...selectedIds, id],
56
- )
57
- }
58
-
59
- return (
60
- <div className="overflow-x-auto rounded-lg border border-[var(--border)]">
61
- <table className="w-full text-left text-sm">
62
- <thead className="bg-muted border-b border-[var(--border)]">
63
- <tr>
64
- <th className="w-10 px-4 py-3">
65
- <input
66
- type="checkbox"
67
- checked={allSelected}
68
- onChange={toggleAll}
69
- className="rounded border-[var(--border)]"
70
- />
71
- </th>
72
- {columns.map((col) => (
73
- <th
74
- key={col.key}
75
- className="px-4 py-3 font-medium"
76
- aria-sort={
77
- col.sortable && onSort
78
- ? sortField === col.key
79
- ? sortDir === 'asc'
80
- ? 'ascending'
81
- : 'descending'
82
- : 'none'
83
- : undefined
84
- }
85
- >
86
- {col.sortable && onSort ? (
87
- <button
88
- onClick={() => onSort(col.key)}
89
- className="inline-flex items-center gap-1 hover:text-[var(--foreground)]"
90
- >
91
- {col.label}
92
- {sortField === col.key && (
93
- <span className="text-xs">{sortDir === 'asc' ? '↑' : '↓'}</span>
94
- )}
95
- </button>
96
- ) : (
97
- col.label
98
- )}
99
- </th>
100
- ))}
101
- {rowActions && rowActions.length > 0 && <th className="w-20 px-4 py-3" />}
102
- </tr>
103
- </thead>
104
- <tbody className="divide-y divide-[var(--border)]">
105
- {rows.map((row) => (
106
- <tr
107
- key={row.id}
108
- className={`hover:bg-[var(--accent)] ${selectedIds.includes(row.id) ? 'bg-[var(--accent)]' : ''}`}
109
- onClick={() => onRowClick?.(row)}
110
- style={onRowClick ? { cursor: 'pointer' } : undefined}
111
- >
112
- <td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
113
- <input
114
- type="checkbox"
115
- checked={selectedIds.includes(row.id)}
116
- onChange={() => toggleRow(row.id)}
117
- className="rounded border-[var(--border)]"
118
- />
119
- </td>
120
- {columns.map((col) => (
121
- <td key={col.key} className="px-4 py-3">
122
- {row[col.key]}
123
- </td>
124
- ))}
125
- {rowActions && rowActions.length > 0 && (
126
- <td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
127
- <RowActionsMenu actions={rowActions} row={row} />
128
- </td>
129
- )}
130
- </tr>
131
- ))}
132
- </tbody>
133
- </table>
134
- </div>
135
- )
136
- }
137
-
138
- function RowActionsMenu({ actions, row }: { actions: RowAction[]; row: Row }) {
139
- const [open, setOpen] = useState(false)
140
- const ref = useRef<HTMLDivElement>(null)
141
-
142
- useEffect(() => {
143
- if (!open) return
144
- function handleClickOutside(e: MouseEvent) {
145
- if (ref.current && !ref.current.contains(e.target as Node)) {
146
- setOpen(false)
147
- }
148
- }
149
- document.addEventListener('mousedown', handleClickOutside)
150
- return () => document.removeEventListener('mousedown', handleClickOutside)
151
- }, [open])
152
-
153
- return (
154
- <div className="relative flex items-center justify-end gap-1" ref={ref}>
155
- {actions
156
- .filter((a) => a.icon)
157
- .map((action) => (
158
- <button
159
- key={action.key}
160
- onClick={() => action.onClick(row)}
161
- className="hover:bg-muted rounded p-1 text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
162
- aria-label={action.label}
163
- title={action.label}
164
- >
165
- {action.icon}
166
- </button>
167
- ))}
168
- <button
169
- onClick={() => setOpen((o) => !o)}
170
- className="hover:bg-muted rounded p-1"
171
- aria-label="More actions"
172
- >
173
- <MoreVerticalIcon />
174
- </button>
175
- {open && (
176
- <div className="absolute top-full right-0 z-50 mt-1 w-40 rounded-md border border-[var(--border)] bg-[var(--popover)] py-1 shadow-lg">
177
- {actions.map((action) => (
178
- <button
179
- key={action.key}
180
- className={`flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-[var(--accent)] ${
181
- action.destructive ? 'text-[var(--destructive)]' : ''
182
- }`}
183
- onClick={() => {
184
- action.onClick(row)
185
- setOpen(false)
186
- }}
187
- >
188
- {action.icon && <span className="h-4 w-4">{action.icon}</span>}
189
- {action.label}
190
- </button>
191
- ))}
192
- </div>
193
- )}
194
- </div>
195
- )
196
- }
197
-
198
- function MoreVerticalIcon() {
199
- return (
200
- <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
201
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 5v.01M12 12v.01M12 19v.01" />
202
- </svg>
203
- )
204
- }
@@ -1,94 +0,0 @@
1
- 'use client'
2
-
3
- export interface PaginationProps {
4
- page: number
5
- perPage: number
6
- total: number
7
- onPageChange: (page: number) => void
8
- onPerPageChange: (perPage: number) => void
9
- }
10
-
11
- export function Pagination({
12
- page,
13
- perPage,
14
- total,
15
- onPageChange,
16
- onPerPageChange,
17
- }: PaginationProps) {
18
- const totalPages = Math.max(1, Math.ceil(total / perPage))
19
- const start = (page - 1) * perPage + 1
20
- const end = Math.min(page * perPage, total)
21
-
22
- function pageNumbers(): (number | 'ellipsis')[] {
23
- const pages: (number | 'ellipsis')[] = []
24
- for (let i = 1; i <= totalPages; i++) {
25
- if (i === 1 || i === totalPages || Math.abs(i - page) <= 1) {
26
- pages.push(i)
27
- } else if (pages[pages.length - 1] !== 'ellipsis') {
28
- pages.push('ellipsis')
29
- }
30
- }
31
- return pages
32
- }
33
-
34
- return (
35
- <div className="flex items-center justify-between text-sm">
36
- <span className="text-[var(--muted-foreground)]">
37
- {start}–{end} of {total} results
38
- </span>
39
-
40
- <div className="flex items-center gap-2">
41
- <select
42
- value={perPage}
43
- onChange={(e) => {
44
- onPerPageChange(Number(e.target.value))
45
- onPageChange(1)
46
- }}
47
- className="rounded-md border border-[var(--border)] bg-[var(--input-background)] px-2 py-1 text-sm"
48
- >
49
- {[10, 25, 50, 100].map((n) => (
50
- <option key={n} value={n}>
51
- {n} / page
52
- </option>
53
- ))}
54
- </select>
55
-
56
- <nav className="flex items-center gap-1">
57
- <button
58
- onClick={() => onPageChange(page - 1)}
59
- disabled={page <= 1}
60
- className="rounded-md px-2 py-1 hover:bg-[var(--accent)] disabled:opacity-50"
61
- >
62
- Prev
63
- </button>
64
- {pageNumbers().map((p, i) =>
65
- p === 'ellipsis' ? (
66
- <span key={`e${i}`} className="px-1 text-[var(--muted-foreground)]">
67
-
68
- </span>
69
- ) : (
70
- <button
71
- key={p}
72
- onClick={() => onPageChange(p)}
73
- className={`min-w-[2rem] rounded-md px-2 py-1 ${
74
- p === page
75
- ? 'bg-[var(--primary)] text-[var(--primary-foreground)]'
76
- : 'hover:bg-[var(--accent)]'
77
- }`}
78
- >
79
- {p}
80
- </button>
81
- ),
82
- )}
83
- <button
84
- onClick={() => onPageChange(page + 1)}
85
- disabled={page >= totalPages}
86
- className="rounded-md px-2 py-1 hover:bg-[var(--accent)] disabled:opacity-50"
87
- >
88
- Next
89
- </button>
90
- </nav>
91
- </div>
92
- </div>
93
- )
94
- }
@@ -1,53 +0,0 @@
1
- import { forwardRef, type SelectHTMLAttributes, type ReactNode } from 'react'
2
- import { cv, type VariantProps } from '../../lib/cv.js'
3
- import { formControlBase, formControlState } from './form-control.js'
4
-
5
- const select = cv(`${formControlBase} appearance-none pr-8`, {
6
- variants: {
7
- size: {
8
- sm: 'py-1.5 pr-7 pl-3 text-[length:var(--fs-sub)]',
9
- md: '',
10
- lg: 'py-2.5 pr-9 pl-3',
11
- },
12
- state: {
13
- default: formControlState.default,
14
- invalid: formControlState.invalid,
15
- valid: formControlState.valid,
16
- },
17
- },
18
- defaultVariants: { size: 'md', state: 'default' },
19
- })
20
-
21
- export type SelectVariants = VariantProps<typeof select>
22
-
23
- export interface SelectProps
24
- extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'>, SelectVariants {
25
- children?: ReactNode
26
- }
27
-
28
- export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
29
- { size, state, className, children, ...rest },
30
- ref,
31
- ) {
32
- return (
33
- <div className="relative">
34
- <select ref={ref} className={select({ size, state, class: className })} {...rest}>
35
- {children}
36
- </select>
37
- <svg
38
- aria-hidden
39
- className="pointer-events-none absolute top-1/2 right-2 h-4 w-4 -translate-y-1/2 text-[var(--muted)]"
40
- viewBox="0 0 20 20"
41
- fill="currentColor"
42
- >
43
- <path
44
- fillRule="evenodd"
45
- d="M5.23 7.21a.75.75 0 011.06.02L10 11.06l3.71-3.83a.75.75 0 111.08 1.04l-4.25 4.39a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z"
46
- clipRule="evenodd"
47
- />
48
- </svg>
49
- </div>
50
- )
51
- })
52
-
53
- export { select as selectVariants }
@@ -1,72 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useCallback } from 'react'
4
-
5
- type ToastType = 'success' | 'error' | 'warning' | 'info'
6
-
7
- interface Toast {
8
- id: string
9
- type: ToastType
10
- message: string
11
- }
12
-
13
- const typeClasses: Record<ToastType, string> = {
14
- success: 'border-success bg-success/10 text-success',
15
- error: 'border-destructive bg-destructive/10 text-destructive',
16
- warning: 'border-warning bg-warning/10 text-warning',
17
- info: 'border-info bg-info/10 text-info',
18
- }
19
-
20
- export function useToast() {
21
- const [toasts, setToasts] = useState<Toast[]>([])
22
-
23
- const addToast = useCallback((type: ToastType, message: string) => {
24
- const id = crypto.randomUUID()
25
- setToasts((prev) => [...prev, { id, type, message }])
26
- setTimeout(() => {
27
- setToasts((prev) => prev.filter((t) => t.id !== id))
28
- }, 5000)
29
- }, [])
30
-
31
- const dismissToast = useCallback((id: string) => {
32
- setToasts((prev) => prev.filter((t) => t.id !== id))
33
- }, [])
34
-
35
- return { toasts, addToast, dismissToast }
36
- }
37
-
38
- export interface ToastContainerProps {
39
- toasts: Toast[]
40
- onDismiss: (id: string) => void
41
- }
42
-
43
- export function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {
44
- if (toasts.length === 0) return null
45
-
46
- return (
47
- <div className="fixed right-4 bottom-4 z-[100] flex flex-col gap-2">
48
- {toasts.map((toast) => (
49
- <div
50
- key={toast.id}
51
- className={`flex items-center gap-3 rounded-md border-l-4 px-4 py-3 shadow-lg ${typeClasses[toast.type]}`}
52
- >
53
- <p className="flex-1 text-sm">{toast.message}</p>
54
- <button
55
- onClick={() => onDismiss(toast.id)}
56
- className="shrink-0 opacity-60 hover:opacity-100"
57
- >
58
- <svg
59
- className="h-4 w-4"
60
- fill="none"
61
- viewBox="0 0 24 24"
62
- stroke="currentColor"
63
- strokeWidth={2}
64
- >
65
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
66
- </svg>
67
- </button>
68
- </div>
69
- ))}
70
- </div>
71
- )
72
- }
@@ -1,61 +0,0 @@
1
- export { Button, buttonVariants } from './Button.js'
2
- export type { ButtonProps, ButtonVariants } from './Button.js'
3
- export { Badge, badgeVariants, STATUS_TONE, STATUS_LABEL } from './Badge.js'
4
- export type { BadgeProps, BadgeVariants, DocumentStatus } from './Badge.js'
5
- export {
6
- Card,
7
- CardHeader,
8
- CardTitle,
9
- CardDescription,
10
- CardContent,
11
- CardFooter,
12
- cardVariants,
13
- } from './Card.js'
14
- export type { CardProps, CardVariants } from './Card.js'
15
- export { Input, inputVariants } from './Input.js'
16
- export type { InputProps, InputVariants } from './Input.js'
17
- export { Select, selectVariants } from './Select.js'
18
- export type { SelectProps, SelectVariants } from './Select.js'
19
- export { Avatar } from './Avatar.js'
20
- export type { AvatarProps } from './Avatar.js'
21
- export { EmptyState } from './EmptyState.js'
22
- export type { EmptyStateProps } from './EmptyState.js'
23
- export { Skeleton } from './Skeleton.js'
24
- export type { SkeletonProps } from './Skeleton.js'
25
- export { useToast, ToastContainer } from './Toast.js'
26
- export type { ToastContainerProps } from './Toast.js'
27
- export { Modal } from './Modal.js'
28
- export type { ModalProps } from './Modal.js'
29
- export { ConfirmDialog } from './ConfirmDialog.js'
30
- export type { ConfirmDialogProps } from './ConfirmDialog.js'
31
- export { ReauthDialog } from './ReauthDialog.js'
32
- export type { ReauthDialogProps } from './ReauthDialog.js'
33
- export { DataTable } from './DataTable.js'
34
- export type { DataTableProps } from './DataTable.js'
35
- export { Pagination } from './Pagination.js'
36
- export type { PaginationProps } from './Pagination.js'
37
- export { SearchInput } from './SearchInput.js'
38
- export type { SearchInputProps } from './SearchInput.js'
39
- export { Toggle, toggleTrackVariants, toggleThumbVariants } from './Toggle.js'
40
- export type { ToggleProps, ToggleVariants } from './Toggle.js'
41
- export { StatusBadge, statusBadgeVariants } from './StatusBadge.js'
42
- export type { StatusBadgeProps, StatusBadgeVariants, StatusBadgeTone } from './StatusBadge.js'
43
- export { FilterPill } from './FilterPill.js'
44
- export type { FilterPillProps } from './FilterPill.js'
45
- export { StatPill } from './StatPill.js'
46
- export type { StatPillProps } from './StatPill.js'
47
- export { RowActions, RowActionButton } from './RowActions.js'
48
- export type { RowActionButtonProps } from './RowActions.js'
49
- export {
50
- SplitPaneLayout,
51
- SplitPaneList,
52
- InspectorPane,
53
- InspectorPaneHeader,
54
- InspectorPaneBody,
55
- InspectorPaneSection,
56
- InspectorPaneFooter,
57
- } from './InspectorPane.js'
58
- export type { InspectorPaneProps } from './InspectorPane.js'
59
- export { formControlBase, formControlState } from './form-control.js'
60
- export { CommandPalette } from './CommandPalette.js'
61
- export type { CommandPaletteProps } from './CommandPalette.js'
@@ -1,55 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect, useCallback } from 'react'
4
-
5
- interface ContentLock {
6
- lockedBy: string | null
7
- lockedAt: Date | null
8
- isLocked: boolean
9
- isLockedByMe: boolean
10
- }
11
-
12
- export function useContentLock(
13
- documentId: string | undefined,
14
- userId: string,
15
- ): ContentLock & {
16
- acquireLock: () => Promise<boolean>
17
- releaseLock: () => Promise<void>
18
- } {
19
- const [lock, setLock] = useState<ContentLock>({
20
- lockedBy: null,
21
- lockedAt: null,
22
- isLocked: false,
23
- isLockedByMe: false,
24
- })
25
-
26
- const acquireLock = useCallback(async () => {
27
- if (!documentId) return false
28
- setLock({
29
- lockedBy: userId,
30
- lockedAt: new Date(),
31
- isLocked: true,
32
- isLockedByMe: true,
33
- })
34
- return true
35
- }, [documentId, userId])
36
-
37
- const releaseLock = useCallback(async () => {
38
- setLock({
39
- lockedBy: null,
40
- lockedAt: null,
41
- isLocked: false,
42
- isLockedByMe: false,
43
- })
44
- }, [])
45
-
46
- useEffect(() => {
47
- return () => {
48
- if (lock.isLockedByMe) {
49
- releaseLock()
50
- }
51
- }
52
- }, [lock.isLockedByMe, releaseLock])
53
-
54
- return { ...lock, acquireLock, releaseLock }
55
- }
@@ -1,14 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useEffect } from 'react'
4
-
5
- export function useDebounce<T>(value: T, delayMs: number): T {
6
- const [debouncedValue, setDebouncedValue] = useState(value)
7
-
8
- useEffect(() => {
9
- const timer = setTimeout(() => setDebouncedValue(value), delayMs)
10
- return () => clearTimeout(timer)
11
- }, [value, delayMs])
12
-
13
- return debouncedValue
14
- }