@dyrected/admin 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.
Files changed (95) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE.md +50 -0
  3. package/README.md +73 -0
  4. package/components.json +17 -0
  5. package/eslint.config.js +22 -0
  6. package/index.html +13 -0
  7. package/package.json +99 -0
  8. package/postcss.config.js +6 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/icons.svg +24 -0
  11. package/src/App.css +184 -0
  12. package/src/App.tsx +25 -0
  13. package/src/assets/dyrected.svg +155 -0
  14. package/src/assets/hero.png +0 -0
  15. package/src/assets/react.svg +1 -0
  16. package/src/assets/vite.svg +1 -0
  17. package/src/components/auth/auth-gate.tsx +64 -0
  18. package/src/components/error-boundary.tsx +45 -0
  19. package/src/components/forms/field-renderer.tsx +111 -0
  20. package/src/components/forms/fields/block-builder.tsx +213 -0
  21. package/src/components/forms/fields/date-picker.tsx +60 -0
  22. package/src/components/forms/fields/json-editor.tsx +62 -0
  23. package/src/components/forms/fields/media-picker.tsx +286 -0
  24. package/src/components/forms/fields/multi-select.tsx +145 -0
  25. package/src/components/forms/fields/radio-field.tsx +51 -0
  26. package/src/components/forms/fields/relationship-picker.tsx +143 -0
  27. package/src/components/forms/fields/rich-text-editor.tsx +224 -0
  28. package/src/components/forms/fields/select-field.tsx +35 -0
  29. package/src/components/forms/fields/switch-field.tsx +16 -0
  30. package/src/components/forms/fields/text-area-field.tsx +15 -0
  31. package/src/components/forms/fields/text-field.tsx +24 -0
  32. package/src/components/forms/form-engine.tsx +87 -0
  33. package/src/components/forms/form-field-renderer.tsx +269 -0
  34. package/src/components/forms/utils.ts +97 -0
  35. package/src/components/layout/admin-shell.tsx +479 -0
  36. package/src/components/layout/branding-provider.tsx +112 -0
  37. package/src/components/live-preview/LivePreviewPane.tsx +128 -0
  38. package/src/components/media/focal-point-picker.tsx +66 -0
  39. package/src/components/media/media-card.tsx +44 -0
  40. package/src/components/media/media-grid.tsx +32 -0
  41. package/src/components/media/media-library-dialog.tsx +465 -0
  42. package/src/components/ui/aspect-ratio.tsx +7 -0
  43. package/src/components/ui/badge.tsx +36 -0
  44. package/src/components/ui/button.tsx +56 -0
  45. package/src/components/ui/calendar.tsx +214 -0
  46. package/src/components/ui/card.tsx +79 -0
  47. package/src/components/ui/checkbox.tsx +28 -0
  48. package/src/components/ui/command.tsx +151 -0
  49. package/src/components/ui/data-table.tsx +219 -0
  50. package/src/components/ui/dialog.tsx +122 -0
  51. package/src/components/ui/dropdown-menu.tsx +200 -0
  52. package/src/components/ui/form.tsx +178 -0
  53. package/src/components/ui/input.tsx +24 -0
  54. package/src/components/ui/label.tsx +24 -0
  55. package/src/components/ui/page-header.tsx +30 -0
  56. package/src/components/ui/pagination.tsx +57 -0
  57. package/src/components/ui/popover.tsx +29 -0
  58. package/src/components/ui/progress.tsx +26 -0
  59. package/src/components/ui/radio-group.tsx +42 -0
  60. package/src/components/ui/render-cell.tsx +110 -0
  61. package/src/components/ui/scroll-area.tsx +46 -0
  62. package/src/components/ui/select.tsx +160 -0
  63. package/src/components/ui/separator.tsx +29 -0
  64. package/src/components/ui/sheet.tsx +140 -0
  65. package/src/components/ui/sidebar.tsx +771 -0
  66. package/src/components/ui/skeleton.tsx +15 -0
  67. package/src/components/ui/sonner.tsx +27 -0
  68. package/src/components/ui/switch.tsx +27 -0
  69. package/src/components/ui/table.tsx +117 -0
  70. package/src/components/ui/tabs.tsx +53 -0
  71. package/src/components/ui/textarea.tsx +22 -0
  72. package/src/components/ui/toggle.tsx +43 -0
  73. package/src/components/ui/tooltip.tsx +28 -0
  74. package/src/hooks/use-mobile.tsx +19 -0
  75. package/src/hooks/use-preferences.ts +56 -0
  76. package/src/index.css +111 -0
  77. package/src/index.tsx +198 -0
  78. package/src/lib/utils.ts +32 -0
  79. package/src/main.tsx +10 -0
  80. package/src/pages/auth/first-user-page.tsx +115 -0
  81. package/src/pages/auth/login-page.tsx +91 -0
  82. package/src/pages/collections/edit-page.tsx +280 -0
  83. package/src/pages/collections/list-page.tsx +343 -0
  84. package/src/pages/dashboard/dashboard.tsx +150 -0
  85. package/src/pages/globals/editor-page.tsx +122 -0
  86. package/src/pages/media/media-page.tsx +564 -0
  87. package/src/pages/setup/setup-prompt.tsx +152 -0
  88. package/src/providers/dyrected-provider.tsx +122 -0
  89. package/src/providers/query-provider.tsx +19 -0
  90. package/src/types/jexl.d.ts +11 -0
  91. package/tailwind.config.ts +102 -0
  92. package/tsconfig.app.json +29 -0
  93. package/tsconfig.json +12 -0
  94. package/tsconfig.node.json +27 -0
  95. package/vite.config.ts +36 -0
@@ -0,0 +1,128 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Button } from '../ui/button';
3
+ import { ExternalLink, Smartphone, Monitor, RotateCcw } from 'lucide-react';
4
+
5
+ interface LivePreviewPaneProps {
6
+ previewUrl: string;
7
+ data: any;
8
+ mode?: 'postMessage' | 'token';
9
+ }
10
+
11
+ export function LivePreviewPane({ previewUrl, data, mode = 'postMessage' }: LivePreviewPaneProps) {
12
+ const iframeRef = useRef<HTMLIFrameElement>(null);
13
+ const [isReady, setIsReady] = useState(false);
14
+ const [viewMode, setViewMode] = useState<'desktop' | 'mobile'>('desktop');
15
+ const [zoom, setZoom] = useState(0.50); // 85% zoom by default for desktop
16
+
17
+ // Handle postMessage communication
18
+ useEffect(() => {
19
+ if (mode !== 'postMessage') return;
20
+
21
+ const handleMessage = (event: MessageEvent) => {
22
+ if (event.data?.type === 'dyrected-live-preview-ready') {
23
+ setIsReady(true);
24
+ // Send initial data once ready
25
+ iframeRef.current?.contentWindow?.postMessage(
26
+ { type: 'dyrected-live-preview', data },
27
+ '*'
28
+ );
29
+ }
30
+ };
31
+
32
+ window.addEventListener('message', handleMessage);
33
+ return () => window.removeEventListener('message', handleMessage);
34
+ }, [mode, data]);
35
+
36
+ // Sync data whenever it changes
37
+ useEffect(() => {
38
+ if (mode === 'postMessage' && isReady) {
39
+ iframeRef.current?.contentWindow?.postMessage(
40
+ { type: 'dyrected-live-preview', data },
41
+ '*'
42
+ );
43
+ }
44
+ }, [data, isReady, mode]);
45
+
46
+ const reload = () => {
47
+ if (iframeRef.current) {
48
+ iframeRef.current.src = previewUrl;
49
+ setIsReady(false);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className="flex flex-col h-full bg-muted/20 border-l border-border/60">
55
+ <div className="flex items-center justify-between px-4 py-2 bg-white border-b border-border/40">
56
+ <div className="flex items-center gap-2">
57
+ <Button
58
+ variant="ghost"
59
+ size="icon"
60
+ className={`h-8 w-8 ${viewMode === 'desktop' ? 'bg-muted' : ''}`}
61
+ onClick={() => setViewMode('desktop')}
62
+ >
63
+ <Monitor className="h-4 w-4" />
64
+ </Button>
65
+ <Button
66
+ variant="ghost"
67
+ size="icon"
68
+ className={`h-8 w-8 ${viewMode === 'mobile' ? 'bg-muted' : ''}`}
69
+ onClick={() => setViewMode('mobile')}
70
+ >
71
+ <Smartphone className="h-4 w-4" />
72
+ </Button>
73
+
74
+ {viewMode === 'desktop' && (
75
+ <div className="flex items-center gap-1 ml-2 pl-2 border-l border-border/40">
76
+ <span className="text-[10px] font-bold text-muted-foreground/50 uppercase tracking-wider mr-1">Zoom</span>
77
+ {[0.50, 0.75, 1.0].map((z) => (
78
+ <Button
79
+ key={z}
80
+ variant="ghost"
81
+ size="sm"
82
+ className={`h-7 px-2 text-[10px] font-medium ${zoom === z ? 'bg-primary/10 text-primary' : 'text-muted-foreground/60'}`}
83
+ onClick={() => setZoom(z)}
84
+ >
85
+ {Math.round(z * 100)}%
86
+ </Button>
87
+ ))}
88
+ </div>
89
+ )}
90
+ </div>
91
+
92
+ <div className="flex items-center gap-2">
93
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={reload}>
94
+ <RotateCcw className="h-3.5 w-3.5" />
95
+ </Button>
96
+ <Button variant="ghost" size="icon" className="h-8 w-8" asChild>
97
+ <a href={previewUrl} target="_blank" rel="noreferrer">
98
+ <ExternalLink className="h-3.5 w-3.5" />
99
+ </a>
100
+ </Button>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="flex-1 flex items-center justify-center p-0 overflow-hidden bg-muted/5">
105
+ <div
106
+ className={`bg-white shadow-[0_20px_50px_rgba(0,0,0,0.1)] transition-all duration-500 overflow-hidden border border-border/40 ${viewMode === 'mobile' ? 'w-[375px] h-[667px]' : 'w-full h-full'
107
+ }`}
108
+ >
109
+ <iframe
110
+ ref={iframeRef}
111
+ src={previewUrl}
112
+ className="border-none transition-transform duration-300"
113
+ style={viewMode === 'desktop' ? {
114
+ width: `${100 / zoom}%`,
115
+ height: `${100 / zoom}%`,
116
+ transform: `scale(${zoom})`,
117
+ transformOrigin: 'top left',
118
+ } : {
119
+ width: '100%',
120
+ height: '100%',
121
+ }}
122
+ title="Live Preview"
123
+ />
124
+ </div>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ interface FocalPoint {
5
+ x: number;
6
+ y: number;
7
+ }
8
+
9
+ interface FocalPointPickerProps {
10
+ url: string;
11
+ value?: FocalPoint;
12
+ onChange: (value: FocalPoint) => void;
13
+ className?: string;
14
+ }
15
+
16
+ export function FocalPointPicker({ url, value, onChange, className }: FocalPointPickerProps) {
17
+ const containerRef = React.useRef<HTMLDivElement>(null);
18
+
19
+ const focalPoint = value || { x: 50, y: 50 };
20
+
21
+ const handleClick = (e: React.MouseEvent) => {
22
+ if (!containerRef.current) return;
23
+ const rect = containerRef.current.getBoundingClientRect();
24
+ const x = Math.round(((e.clientX - rect.left) / rect.width) * 100);
25
+ const y = Math.round(((e.clientY - rect.top) / rect.height) * 100);
26
+
27
+ // Clamp values between 0 and 100
28
+ const clampedX = Math.max(0, Math.min(100, x));
29
+ const clampedY = Math.max(0, Math.min(100, y));
30
+
31
+ onChange({ x: clampedX, y: clampedY });
32
+ };
33
+
34
+ return (
35
+ <div className="space-y-2">
36
+ <div
37
+ ref={containerRef}
38
+ className={cn("relative cursor-crosshair overflow-hidden rounded-xl border border-border/40 bg-muted/20 group", className)}
39
+ onClick={handleClick}
40
+ >
41
+ <img
42
+ src={url}
43
+ alt="Focal point picker"
44
+ className="w-full h-auto pointer-events-none select-none max-h-[400px] object-contain bg-checkered"
45
+ />
46
+
47
+ {/* Focal point indicator */}
48
+ <div
49
+ className="absolute w-8 h-8 -ml-4 -mt-4 border-2 border-white rounded-full shadow-2xl flex items-center justify-center pointer-events-none transition-all duration-200 ease-out"
50
+ style={{ left: `${focalPoint.x}%`, top: `${focalPoint.y}%` }}
51
+ >
52
+ <div className="w-1.5 h-1.5 bg-white rounded-full shadow-sm" />
53
+ <div className="absolute w-full h-px bg-white/40" />
54
+ <div className="absolute h-full w-px bg-white/40" />
55
+ </div>
56
+
57
+ <div className="absolute bottom-3 left-3 bg-black/60 backdrop-blur-md px-2.5 py-1 rounded-lg text-[10px] text-white font-bold tracking-widest border border-white/10 opacity-0 group-hover:opacity-100 transition-opacity">
58
+ X: {focalPoint.x}% / Y: {focalPoint.y}%
59
+ </div>
60
+ </div>
61
+ <p className="text-[10px] text-muted-foreground font-medium px-1">
62
+ Click on the image to set the focal point for smart cropping.
63
+ </p>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,44 @@
1
+ import { Link } from "react-router-dom"
2
+ import { Pencil, Trash2 } from "lucide-react"
3
+ import { Button } from "../ui/button"
4
+ import { getMediaUrl } from "../../lib/utils"
5
+
6
+ interface MediaCardProps {
7
+ item: any
8
+ baseUrl: string
9
+ onDelete: (id: string) => void
10
+ editPath: string
11
+ }
12
+
13
+ export function MediaCard({ item, baseUrl, onDelete, editPath }: MediaCardProps) {
14
+ const url = getMediaUrl(item, baseUrl)
15
+
16
+ return (
17
+ <div className="group relative aspect-square rounded-2xl overflow-hidden bg-white border border-border/40 shadow-sm hover:shadow-xl hover:border-primary/20 transition-all duration-300">
18
+ <img
19
+ src={url}
20
+ alt={item.filename}
21
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
22
+ />
23
+ <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center gap-3 backdrop-blur-[2px]">
24
+ <Link to={editPath}>
25
+ <Button size="icon" variant="secondary" className="h-9 w-9 rounded-full bg-white/90 hover:bg-white text-foreground shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300">
26
+ <Pencil className="h-4 w-4" />
27
+ </Button>
28
+ </Link>
29
+ <Button
30
+ size="icon"
31
+ variant="destructive"
32
+ className="h-9 w-9 rounded-full bg-destructive/90 hover:bg-destructive shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform duration-300 delay-75"
33
+ onClick={() => onDelete(item.id)}
34
+ >
35
+ <Trash2 className="h-4 w-4" />
36
+ </Button>
37
+ </div>
38
+ <div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
39
+ <p className="text-[10px] text-white truncate font-medium">{item.filename}</p>
40
+ <p className="text-[8px] text-white/60 uppercase tracking-wider mt-0.5">{item.mimeType}</p>
41
+ </div>
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,32 @@
1
+ import { MediaCard } from "./media-card"
2
+
3
+ interface MediaGridProps {
4
+ items: any[]
5
+ baseUrl: string
6
+ onDelete: (id: string) => void
7
+ slug: string
8
+ }
9
+
10
+ export function MediaGrid({ items, baseUrl, onDelete, slug }: MediaGridProps) {
11
+ if (!items || items.length === 0) {
12
+ return (
13
+ <div className="flex flex-col items-center justify-center h-[300px] border-2 border-dashed border-border/60 rounded-3xl bg-muted/5">
14
+ <p className="text-sm text-muted-foreground font-medium">No media assets found</p>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ return (
20
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
21
+ {items.map((item) => (
22
+ <MediaCard
23
+ key={item.id}
24
+ item={item}
25
+ baseUrl={baseUrl}
26
+ onDelete={onDelete}
27
+ editPath={`/collections/${slug}/edit/${item.id}`}
28
+ />
29
+ ))}
30
+ </div>
31
+ )
32
+ }