react-admin-crud-manager 1.0.0

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.
@@ -0,0 +1,181 @@
1
+ import React from "react";
2
+ import Select from "./Select";
3
+ import Switch from "./Switch";
4
+ import PhoneInput from "./PhoneInput";
5
+ import { TextArea } from "./TextArea";
6
+ import ImagePicker from "./ImagePicker";
7
+ import { Input } from "./Input";
8
+ import TinyEditor from "./TinyEditor";
9
+ import Checkbox from "./Checkbox";
10
+
11
+ const RenderFields = ({ field, formData, handleChange }) => {
12
+ const {
13
+ key,
14
+ label,
15
+ type,
16
+ options,
17
+ placeholder,
18
+ rows,
19
+ inputClass,
20
+ search,
21
+ accept,
22
+ text,
23
+ required = false,
24
+ minLength,
25
+ dragDrop,
26
+ parentClass,
27
+ countriesList,
28
+ defaultCountry,
29
+ multiple,
30
+ dropdownMaxHeight,
31
+ editorKey,
32
+ fontFamily,
33
+ disabled,
34
+ } = field;
35
+
36
+ let value = formData?.[key];
37
+ if (value === undefined || value === null) {
38
+ value = "";
39
+ }
40
+
41
+ const finalPlaceholder =
42
+ placeholder || (type === "select" ? `Select ${label}` : `Enter ${label}`);
43
+
44
+ const baseClass =
45
+ "w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-sm focus:outline-none focus:ring-1 focus:ring-blue-200 bg-white text-black dark:bg-gray-700 dark:text-white";
46
+
47
+ switch (type) {
48
+ case "select":
49
+ return (
50
+ <Select
51
+ options={options || []}
52
+ value={value}
53
+ onChange={(val) => handleChange(key, val)}
54
+ placeholder={finalPlaceholder}
55
+ className={inputClass || ""}
56
+ search={search}
57
+ required={required}
58
+ label={label}
59
+ name={key}
60
+ disabled={disabled}
61
+ parentClass={parentClass}
62
+ multiple={multiple}
63
+ dropdownMaxHeight={dropdownMaxHeight}
64
+ />
65
+ );
66
+
67
+ case "checkbox":
68
+ return (
69
+ <Checkbox
70
+ name={key}
71
+ label={label}
72
+ options={options || []}
73
+ value={value}
74
+ onChange={(val) => handleChange(key, val)}
75
+ required={required}
76
+ parentClass={parentClass}
77
+ className={inputClass || ""}
78
+ multiSelect={multiple}
79
+ disabled={disabled}
80
+ />
81
+ );
82
+
83
+ case "switch":
84
+ return (
85
+ <Switch
86
+ value={value}
87
+ onChange={(val) => handleChange(key, val)}
88
+ text={text}
89
+ options={options || []}
90
+ label={label}
91
+ required={required}
92
+ name={key}
93
+ disabled={disabled}
94
+ parentClass={parentClass}
95
+ />
96
+ );
97
+
98
+ case "phone":
99
+ return (
100
+ <PhoneInput
101
+ value={value}
102
+ onChange={(val) => handleChange(key, val)}
103
+ countriesList={countriesList}
104
+ defaultCountry={defaultCountry}
105
+ required={required}
106
+ placeholder={finalPlaceholder}
107
+ search={search}
108
+ label={label}
109
+ name={key}
110
+ disabled={disabled}
111
+ parentClass={parentClass}
112
+ />
113
+ );
114
+
115
+ case "textarea":
116
+ return (
117
+ <TextArea
118
+ value={value}
119
+ onChange={(e) => handleChange(key, e.target.value)}
120
+ placeholder={finalPlaceholder}
121
+ rows={rows || 3}
122
+ className={`${baseClass} ${inputClass || ""}`}
123
+ required={required}
124
+ name={key}
125
+ label={label}
126
+ disabled={disabled}
127
+ parentClass={parentClass}
128
+ />
129
+ );
130
+
131
+ case "image":
132
+ return (
133
+ <ImagePicker
134
+ value={value}
135
+ onChange={(imgObj) => handleChange(key, imgObj)}
136
+ required={required}
137
+ accept={accept || "image/*"}
138
+ id={`file-${key}`}
139
+ dragDrop={dragDrop}
140
+ label={label}
141
+ name={key}
142
+ parentClass={parentClass}
143
+ />
144
+ );
145
+
146
+ case "tinyEditor":
147
+ return (
148
+ <TinyEditor
149
+ value={value}
150
+ onChange={(newValue) => handleChange(key, newValue)}
151
+ required={required}
152
+ key={`editor-${key}`}
153
+ placeholder={finalPlaceholder}
154
+ label={label}
155
+ parentClass={parentClass}
156
+ fontFamily={fontFamily}
157
+ editorKey={editorKey}
158
+ disabled={disabled}
159
+ />
160
+ );
161
+
162
+ default:
163
+ return (
164
+ <Input
165
+ type={type || "text"}
166
+ value={value}
167
+ onChange={(e) => handleChange(key, e.target.value)}
168
+ placeholder={finalPlaceholder}
169
+ className={`${baseClass} ${inputClass || ""}`}
170
+ required={required}
171
+ name={key}
172
+ minLength={minLength}
173
+ label={label}
174
+ parentClass={parentClass}
175
+ disabled={disabled}
176
+ />
177
+ );
178
+ }
179
+ };
180
+
181
+ export default RenderFields;
@@ -0,0 +1,191 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { ChevronDown, Search, Check } from "lucide-react";
3
+ import InputLabel from "./InputLabel";
4
+
5
+ const Select = ({
6
+ options = [],
7
+ value,
8
+ onChange,
9
+ placeholder = "Select option",
10
+ className = "",
11
+ disabled = false,
12
+ search = false,
13
+ label = "",
14
+ required = false,
15
+ name = "",
16
+ parentClass = "",
17
+ multiple = false, // ✅ NEW
18
+ dropdownMaxHeight = "",
19
+ }) => {
20
+ const [open, setOpen] = useState(false);
21
+ const [searchTerm, setSearchTerm] = useState("");
22
+ const [showAboveState, setShowAboveState] = useState(true);
23
+ const dropdownRef = useRef(null);
24
+ const searchInputRef = useRef(null);
25
+
26
+ const normalize = (val) =>
27
+ typeof val === "boolean" ? String(val) : String(val ?? "");
28
+
29
+ const normalizedValue = multiple
30
+ ? (value || []).map(normalize)
31
+ : normalize(value);
32
+
33
+ const filteredOptions = options.filter((option) =>
34
+ option.label.toLowerCase().includes(searchTerm.toLowerCase()),
35
+ );
36
+
37
+ const isSelected = (optionValue) => {
38
+ const normalized = normalize(optionValue);
39
+ if (multiple) {
40
+ return normalizedValue.includes(normalized);
41
+ }
42
+ return normalized === normalizedValue;
43
+ };
44
+
45
+ useEffect(() => {
46
+ const handleClickOutside = (event) => {
47
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
48
+ setOpen(false);
49
+ setSearchTerm("");
50
+ }
51
+ };
52
+ document.addEventListener("mousedown", handleClickOutside);
53
+ return () => document.removeEventListener("mousedown", handleClickOutside);
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ if (open && dropdownRef.current) {
58
+ const rect = dropdownRef.current.getBoundingClientRect();
59
+ const viewportHeight = window.innerHeight;
60
+ const spaceBelow = viewportHeight - rect.bottom;
61
+ const dropdownHeight = 200;
62
+ setShowAboveState(spaceBelow < dropdownHeight);
63
+ }
64
+ }, [open]);
65
+
66
+ useEffect(() => {
67
+ if (open && searchInputRef.current) {
68
+ searchInputRef.current.focus();
69
+ }
70
+ }, [open]);
71
+
72
+ const handleOptionClick = (optionValue) => {
73
+ let finalValue = optionValue;
74
+ if (optionValue === "true") finalValue = true;
75
+ else if (optionValue === "false") finalValue = false;
76
+
77
+ if (multiple) {
78
+ const exists = normalizedValue.includes(normalize(optionValue));
79
+
80
+ let newValues;
81
+ if (exists) {
82
+ newValues = value.filter(
83
+ (v) => normalize(v) !== normalize(optionValue),
84
+ );
85
+ } else {
86
+ newValues = [...(value || []), finalValue];
87
+ }
88
+
89
+ onChange(newValues);
90
+ } else {
91
+ onChange(finalValue);
92
+ setOpen(false);
93
+ }
94
+
95
+ setSearchTerm("");
96
+ };
97
+
98
+ const selectedLabels = multiple
99
+ ? options
100
+ .filter((opt) => isSelected(opt.value))
101
+ .map((opt) => opt.label)
102
+ .join(", ")
103
+ : options.find((opt) => isSelected(opt.value))?.label;
104
+
105
+ return (
106
+ <div key={name} className={parentClass || "col-span-12"}>
107
+ <InputLabel label={label} required={required} />
108
+
109
+ <div className={`relative ${className}`} ref={dropdownRef}>
110
+ <button
111
+ type="button"
112
+ onClick={() => !disabled && setOpen(!open)}
113
+ disabled={disabled}
114
+ className={`w-full h-10 px-3 border border-gray-300 dark:border-gray-600 rounded-md text-left text-sm flex items-center justify-between
115
+ ${
116
+ !selectedLabels
117
+ ? "text-gray-500 dark:text-gray-400"
118
+ : "dark:text-white"
119
+ }
120
+ ${disabled ? "opacity-50 cursor-not-allowed" : "dark:bg-gray-700"}`}
121
+ >
122
+ <span className="truncate">{selectedLabels || placeholder}</span>
123
+
124
+ <ChevronDown
125
+ className={`w-4 h-4 transition-transform ${
126
+ open ? "rotate-180" : ""
127
+ }`}
128
+ />
129
+ </button>
130
+
131
+ {open && (
132
+ <div
133
+ className={`absolute z-50 w-full border rounded-md bg-white dark:bg-gray-700 shadow-lg
134
+ ${showAboveState ? "bottom-full mb-1" : "top-full mt-1"}`}
135
+ >
136
+ {search && (
137
+ <div className="p-2 border-b border-gray-200 dark:border-gray-600">
138
+ <div className="relative">
139
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
140
+ <input
141
+ ref={searchInputRef}
142
+ type="text"
143
+ value={searchTerm}
144
+ onChange={(e) => setSearchTerm(e.target.value)}
145
+ placeholder="Search..."
146
+ className="w-full pl-9 pr-3 py-2 text-sm border rounded-md bg-white dark:bg-gray-800 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none"
147
+ />
148
+ </div>
149
+ </div>
150
+ )}
151
+
152
+ <div
153
+ className={`max-h-40 overflow-y-auto`}
154
+ style={{
155
+ maxHeight: dropdownMaxHeight || "",
156
+ }}
157
+ >
158
+ {filteredOptions.length > 0 ? (
159
+ filteredOptions.map((option) => (
160
+ <button
161
+ key={String(option.value)}
162
+ type="button"
163
+ onClick={() => handleOptionClick(String(option.value))}
164
+ className={`w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-600
165
+ ${
166
+ isSelected(option.value)
167
+ ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"
168
+ : ""
169
+ }`}
170
+ >
171
+ <span>{option.label}</span>
172
+
173
+ {multiple && isSelected(option.value) && (
174
+ <Check className="w-4 h-4" />
175
+ )}
176
+ </button>
177
+ ))
178
+ ) : (
179
+ <div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
180
+ No options found
181
+ </div>
182
+ )}
183
+ </div>
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ export default Select;
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import InputLabel from "./InputLabel";
3
+
4
+ const Switch = ({
5
+ value = true,
6
+ onChange,
7
+ text,
8
+ options = [],
9
+ label,
10
+ required,
11
+ name = "",
12
+ disabled = false,
13
+ parentClass = "",
14
+ }) => {
15
+ const radioOptions =
16
+ options.length > 0
17
+ ? options
18
+ : [
19
+ { label: "Active", value: true },
20
+ { label: "Inactive", value: false },
21
+ ];
22
+
23
+ return (
24
+ <>
25
+ <div key={name} className={parentClass || "col-span-12"}>
26
+ <InputLabel label={label} required={required} />
27
+ <div className="flex items-center justify-between h-10 gap-4 bg-gray-100 dark:bg-gray-700 px-3 rounded-md border border-gray-100 dark:border-gray-600">
28
+ {/* Left Text */}
29
+ {text && (
30
+ <p className="text-xs text-gray-600 dark:text-gray-400 flex-shrink overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]">
31
+ {text}
32
+ </p>
33
+ )}
34
+
35
+ {/* Radio Buttons */}
36
+ <div className="flex items-center gap-6">
37
+ {radioOptions.map((opt, idx) => (
38
+ <label
39
+ key={idx}
40
+ className="flex items-center gap-2 cursor-pointer select-none"
41
+ >
42
+ <input
43
+ type="radio"
44
+ name="switch-field"
45
+ required={required && idx === 0}
46
+ value={opt.value}
47
+ disabled={disabled}
48
+ checked={value === opt.value}
49
+ onChange={() => onChange(opt.value)}
50
+ className="w-4 h-4 border-gray-300 cursor-pointer"
51
+ />
52
+ <span className="text-sm text-gray-700 dark:text-white">
53
+ {opt.label}
54
+ </span>
55
+ </label>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </>
61
+ );
62
+ };
63
+
64
+ export default Switch;
@@ -0,0 +1,31 @@
1
+ import React, { useState } from "react";
2
+ import InputLabel from "./InputLabel";
3
+
4
+ const TextArea = React.forwardRef(
5
+ ({ className = "", label, required, ...props }, ref) => {
6
+ const combinedClassName = `
7
+ placeholder-gray-400 dark:placeholder-gray-400
8
+ ${className}
9
+ `.trim();
10
+
11
+ return (
12
+ <>
13
+ <div key={props.name} className={props.parentClass || "col-span-12"}>
14
+ <InputLabel label={label} required={required} />
15
+ <div className="relative">
16
+ <textarea
17
+ className={combinedClassName}
18
+ ref={ref}
19
+ required={required}
20
+ {...props}
21
+ />
22
+ </div>
23
+ </div>
24
+ </>
25
+ );
26
+ },
27
+ );
28
+
29
+ TextArea.displayName = "TextArea";
30
+
31
+ export { TextArea };
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { Editor } from "@tinymce/tinymce-react";
3
+ import InputLabel from "./InputLabel";
4
+
5
+ const TinyEditor = ({
6
+ key,
7
+ editorKey = "",
8
+ value = "",
9
+ onChange,
10
+ label = "",
11
+ required = false,
12
+ placeholder = "",
13
+ parentClass = "col-span-12",
14
+ height = 400,
15
+ inline = false,
16
+ disabled = false,
17
+ plugins,
18
+ toolbar,
19
+ menubar = false,
20
+ fontFamily = "Inter, sans-serif",
21
+ initConfig = {},
22
+ imageUploadHandler, // ✅ Promise function passed from parent
23
+ }) => {
24
+ const defaultPlugins = [
25
+ "advlist",
26
+ "autolink",
27
+ "lists",
28
+ "link",
29
+ "image",
30
+ "charmap",
31
+ "preview",
32
+ "anchor",
33
+ "searchreplace",
34
+ "visualblocks",
35
+ "code",
36
+ "fullscreen",
37
+ "insertdatetime",
38
+ "media",
39
+ "table",
40
+ "help",
41
+ "wordcount",
42
+ ];
43
+
44
+ const defaultToolbar =
45
+ "undo redo | blocks | bold italic underline forecolor backcolor | " +
46
+ "alignleft aligncenter alignright alignjustify | " +
47
+ "bullist numlist outdent indent | link image media table | " +
48
+ "removeformat | code fullscreen preview";
49
+
50
+ // ✅ Handle Image Upload via Promise
51
+ const handleImageUpload = (blobInfo) => {
52
+ return new Promise((resolve, reject) => {
53
+ if (!imageUploadHandler) {
54
+ // fallback to base64 if no handler provided
55
+ resolve(`data:${blobInfo.blob().type};base64,${blobInfo.base64()}`);
56
+ return;
57
+ }
58
+
59
+ imageUploadHandler(blobInfo)
60
+ .then((url) => {
61
+ if (!url) {
62
+ reject("Upload failed: No URL returned");
63
+ } else {
64
+ resolve(url);
65
+ }
66
+ })
67
+ .catch((error) => {
68
+ reject(
69
+ typeof error === "string"
70
+ ? error
71
+ : error?.message || "Image upload failed",
72
+ );
73
+ });
74
+ });
75
+ };
76
+
77
+ return (
78
+ <div key={key} className={parentClass}>
79
+ {label && <InputLabel label={label} required={required} />}
80
+
81
+ <Editor
82
+ apiKey={editorKey}
83
+ value={value}
84
+ disabled={disabled}
85
+ init={{
86
+ height,
87
+ inline,
88
+ menubar,
89
+ branding: false,
90
+ statusbar: true,
91
+ automatic_uploads: true,
92
+ images_upload_handler: handleImageUpload,
93
+ plugins: plugins ?? defaultPlugins,
94
+ toolbar: toolbar ?? defaultToolbar,
95
+ placeholder,
96
+ content_style: `
97
+ body {
98
+ font-family: ${fontFamily};
99
+ }
100
+ `,
101
+ ...initConfig,
102
+ }}
103
+ onEditorChange={(content) => {
104
+ if (onChange) {
105
+ onChange(content);
106
+ }
107
+ }}
108
+ />
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default TinyEditor;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ const Spinner = ({ size = 'lg', className = '', color = 'border-blue-500' }) => {
4
+ const sizeClasses = {
5
+ sm: 'w-4 h-4 border-2',
6
+ md: 'w-6 h-6 border-4',
7
+ lg: 'w-8 h-8 border-4',
8
+ xl: 'w-12 h-12 border-8',
9
+ };
10
+
11
+ return (
12
+ <div
13
+ className={`rounded-full border-4 border-blue-500 border-t-gray-200 animate-spin ${sizeClasses[size]} ${className}`}
14
+ style={{
15
+ borderTopColor: color,
16
+ }}
17
+ />
18
+ );
19
+ };
20
+
21
+ export default Spinner;