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,99 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { X } from "lucide-react";
3
+ import Button from "../Button/Button";
4
+ import RenderFields from "../Form/components/RenderFields";
5
+
6
+ const FilterDrawer = ({ isOpen, onClose, config, onApply }) => {
7
+ const [filters, setFilters] = useState({});
8
+
9
+ const handleChange = (key, value) => {
10
+ setFilters((prev) => ({ ...prev, [key]: value }));
11
+ };
12
+
13
+ const handleApply = () => {
14
+ onApply?.(filters);
15
+ onClose();
16
+ };
17
+
18
+ const handleReset = () => {
19
+ setFilters({});
20
+ onApply?.({});
21
+ onClose();
22
+ };
23
+
24
+ return (
25
+ <>
26
+ {/* Overlay */}
27
+ <div
28
+ className={`fixed inset-0 bg-black/50 z-40 transition-opacity duration-300 ${
29
+ isOpen
30
+ ? "opacity-100 pointer-events-auto"
31
+ : "opacity-0 pointer-events-none"
32
+ }`}
33
+ onClick={onClose}
34
+ />
35
+
36
+ {/* Drawer */}
37
+ <div
38
+ className={`fixed top-0 right-0 h-full w-[28rem] bg-white dark:bg-gray-900 shadow-2xl z-50 flex flex-col border-l border-gray-200 dark:border-gray-700
39
+ transform transition-transform duration-300 ease-in-out
40
+ ${isOpen ? "translate-x-0" : "translate-x-full"}
41
+ `}
42
+ >
43
+ {/* Header */}
44
+ <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
45
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
46
+ Filters
47
+ </h2>
48
+ <button
49
+ onClick={onClose}
50
+ className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition"
51
+ >
52
+ <X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
53
+ </button>
54
+ </div>
55
+
56
+ {/* Content */}
57
+ <div className="flex-1 overflow-y-auto px-4 py-3">
58
+ {config?.component ? (
59
+ <config.component filters={filters} onFilterChange={handleChange} />
60
+ ) : (
61
+ <div className="space-y-4">
62
+ {config?.fields?.map((field) => (
63
+ <>
64
+ <RenderFields
65
+ key={field.key}
66
+ field={field}
67
+ formData={filters}
68
+ handleChange={handleChange}
69
+ />
70
+ </>
71
+ ))}
72
+ </div>
73
+ )}
74
+ </div>
75
+
76
+ {/* Footer */}
77
+ <div className="flex gap-2 px-4 py-3 border-t border-gray-200 dark:border-gray-700">
78
+ <Button
79
+ onClick={handleApply}
80
+ variant="contained"
81
+ color="primary"
82
+ fullWidth
83
+ >
84
+ Apply Filters
85
+ </Button>
86
+ <Button
87
+ onClick={handleReset}
88
+ variant="contained"
89
+ className="min-w-[150px]"
90
+ >
91
+ Reset
92
+ </Button>
93
+ </div>
94
+ </div>
95
+ </>
96
+ );
97
+ };
98
+
99
+ export default FilterDrawer;
@@ -0,0 +1,51 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import RenderFields from "./components/RenderFields";
3
+
4
+ const Form = ({ config, onSubmit, initialData = {} }) => {
5
+ const { formClass = "grid grid-cols-12 gap-4", formFields = [] } =
6
+ config || {};
7
+
8
+ const [formData, setFormData] = useState(initialData);
9
+
10
+ useEffect(() => {
11
+ setFormData(initialData);
12
+ }, []);
13
+
14
+ const handleChange = (key, value) => {
15
+ setFormData((prev) => ({ ...prev, [key]: value }));
16
+ };
17
+
18
+ const handleSubmit = (e) => {
19
+ e.preventDefault();
20
+ const form = e.target;
21
+
22
+ if (!form.checkValidity()) {
23
+ form.reportValidity();
24
+ return;
25
+ }
26
+
27
+ onSubmit(formData);
28
+ };
29
+
30
+ return (
31
+ <form
32
+ id={config.title?.toLowerCase().includes("edit") ? "editForm" : "addForm"}
33
+ onSubmit={handleSubmit}
34
+ className={formClass}
35
+ noValidate={false} // enables native validation
36
+ >
37
+ {formFields.map((field) => (
38
+ <>
39
+ <RenderFields
40
+ key={field.key}
41
+ field={field}
42
+ formData={formData}
43
+ handleChange={handleChange}
44
+ />
45
+ </>
46
+ ))}
47
+ </form>
48
+ );
49
+ };
50
+
51
+ export default Form;
@@ -0,0 +1,119 @@
1
+ import React from "react";
2
+ import InputLabel from "./InputLabel";
3
+
4
+ const Checkbox = ({
5
+ name = "",
6
+ label = "", // label for single checkbox
7
+ options = [], // array of { label, value } for multiple
8
+ value = null, // boolean for single, array for multiple, or string for single select
9
+ onChange,
10
+ disabled = false,
11
+ required = false,
12
+ parentClass = "col-span-12",
13
+ className = "",
14
+ multiSelect = false, // ✅ if true, only one option can be selected (like radio)
15
+ }) => {
16
+ // Detect if multiple options are present
17
+ const multiple = Array.isArray(options) && options.length > 0;
18
+
19
+ // Check if a value is selected
20
+ const isChecked = (optionValue) => {
21
+ if (multiple) {
22
+ if (!multiSelect) return value === optionValue;
23
+ return Array.isArray(value) && value.includes(optionValue);
24
+ }
25
+ return Boolean(value);
26
+ };
27
+
28
+ // Handle change for single checkbox
29
+ const handleSingleChange = (e) => {
30
+ onChange?.(e.target.checked, name);
31
+ };
32
+
33
+ // Handle change for multiple checkboxes
34
+ const handleMultipleChange = (optionValue, checked) => {
35
+ if (!onChange) return;
36
+
37
+ if (!multiSelect) {
38
+ if (checked) {
39
+ onChange(optionValue, name);
40
+ } else {
41
+ onChange("", name);
42
+ }
43
+ // Only allow one selected
44
+ } else {
45
+ const newValue = Array.isArray(value) ? [...value] : [];
46
+ if (checked) {
47
+ if (!newValue.includes(optionValue)) newValue.push(optionValue);
48
+ } else {
49
+ const index = newValue.indexOf(optionValue);
50
+ if (index > -1) newValue.splice(index, 1);
51
+ }
52
+ onChange(newValue, name);
53
+ }
54
+ };
55
+
56
+ // ---------------- JSX ----------------
57
+ if (multiple) {
58
+ // Multiple options
59
+ return (
60
+ <>
61
+ <div className={`${parentClass}`}>
62
+ <InputLabel label={label} required={required} />
63
+ <div className={`flex flex-col space-y-2`}>
64
+ {options.map((opt, idx) => (
65
+ <div key={opt.value || opt.label} className="flex items-center">
66
+ <input
67
+ type="checkbox"
68
+ name={name}
69
+ value={opt.value}
70
+ checked={isChecked(opt.value)}
71
+ disabled={disabled || opt.disabled}
72
+ required={required && idx === 0}
73
+ onChange={(e) =>
74
+ handleMultipleChange(opt.value, e.target.checked)
75
+ }
76
+ key={name}
77
+ className={`h-4 w-4 cursor-pointer text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 ${className}`}
78
+ />
79
+ {opt.label && (
80
+ <label
81
+ htmlFor={name}
82
+ className="ml-2 text-sm text-gray-700 dark:text-gray-200 select-none"
83
+ >
84
+ {opt.label}
85
+ </label>
86
+ )}
87
+ </div>
88
+ ))}
89
+ </div>
90
+ </div>
91
+ </>
92
+ );
93
+ }
94
+
95
+ // Single checkbox
96
+ return (
97
+ <div className={`flex items-center ${parentClass}`}>
98
+ <input
99
+ type="checkbox"
100
+ name={name}
101
+ checked={isChecked()}
102
+ disabled={disabled}
103
+ required={required}
104
+ onChange={handleSingleChange}
105
+ className={`h-4 w-4 text-blue-600 cursor-pointer border-gray-300 rounded focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 ${className}`}
106
+ />
107
+ {label && (
108
+ <label
109
+ htmlFor={name}
110
+ className="ml-2 text-sm text-gray-700 dark:text-gray-200 select-none"
111
+ >
112
+ {label}
113
+ </label>
114
+ )}
115
+ </div>
116
+ );
117
+ };
118
+
119
+ export default Checkbox;
@@ -0,0 +1,128 @@
1
+ import { Icon } from "@iconify/react";
2
+ import React, { useState, useEffect, useRef } from "react";
3
+ import InputLabel from "./InputLabel";
4
+
5
+ const ImagePicker = ({
6
+ label = "",
7
+ value = null,
8
+ onChange,
9
+ required = false,
10
+ accept = "image/*",
11
+ id,
12
+ dragDrop = false,
13
+ name = "",
14
+ parentClass = "",
15
+ }) => {
16
+ const [image, setImage] = useState(value);
17
+ const [isDragging, setIsDragging] = useState(false);
18
+ const inputRef = useRef(null);
19
+
20
+ useEffect(() => {
21
+ setImage(value);
22
+ }, [value]);
23
+
24
+ const handleFileChange = (files) => {
25
+ if (!files || files.length === 0) {
26
+ setImage(null);
27
+ onChange?.(null);
28
+ return;
29
+ }
30
+
31
+ const file = files[0];
32
+ const preview = URL.createObjectURL(file);
33
+ const imgObj = { file, preview };
34
+
35
+ setImage(imgObj);
36
+ onChange?.(preview);
37
+ };
38
+
39
+ // Drag events
40
+ const handleDragOver = (e) => {
41
+ if (!dragDrop) return;
42
+ e.preventDefault();
43
+ setIsDragging(true);
44
+ };
45
+
46
+ const handleDragLeave = (e) => {
47
+ if (!dragDrop) return;
48
+ e.preventDefault();
49
+ setIsDragging(false);
50
+ };
51
+
52
+ const handleDrop = (e) => {
53
+ if (!dragDrop) return;
54
+ e.preventDefault();
55
+ setIsDragging(false);
56
+ handleFileChange(e.dataTransfer.files);
57
+ };
58
+
59
+ return (
60
+ <>
61
+ <div key={name} className={parentClass || "col-span-12"}>
62
+ <InputLabel label={label} required={required} />
63
+ <div
64
+ className={`relative rounded-md p-2 transition-all ${
65
+ isDragging
66
+ ? "border border-dashed border-blue-500 bg-blue-50 dark:bg-blue-900"
67
+ : "border bg-gray-50 dark:bg-gray-700"
68
+ }`}
69
+ onDragOver={handleDragOver}
70
+ onDragLeave={handleDragLeave}
71
+ onDrop={handleDrop}
72
+ >
73
+ {/* Blur entire content when dragging */}
74
+ <div
75
+ className={`flex items-center space-x-3 transition-all ${
76
+ isDragging ? "filter blur-sm" : ""
77
+ }`}
78
+ >
79
+ {/* Left: preview clickable */}
80
+ <div
81
+ className="cursor-pointer"
82
+ onClick={() => inputRef.current.click()}
83
+ >
84
+ {image ? (
85
+ <img
86
+ src={image.preview || image}
87
+ alt="preview"
88
+ className="object-cover w-20 h-20 rounded-full"
89
+ />
90
+ ) : (
91
+ <>
92
+ <div className="rounded-full bg-gray-200 dark:bg-gray-800 h-20 w-20 flex items-center justify-center">
93
+ <Icon
94
+ icon="ri:image-add-fill"
95
+ className="text-gray-400 w-10 h-10"
96
+ />
97
+ </div>
98
+ </>
99
+ )}
100
+ </div>
101
+
102
+ {/* Right: upload input */}
103
+ <input
104
+ ref={inputRef}
105
+ id={id}
106
+ type="file"
107
+ accept={accept}
108
+ onChange={(e) => handleFileChange(e.target.files)}
109
+ required={required && !image}
110
+ className="inline-flex items-center justify-center p-2 text-gray-400 text-sm file:cursor-pointer"
111
+ />
112
+ </div>
113
+
114
+ {/* Overlay text */}
115
+ {dragDrop && isDragging && (
116
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
117
+ <span className="text-blue-500 font-semibold text-xl">
118
+ Drop here
119
+ </span>
120
+ </div>
121
+ )}
122
+ </div>
123
+ </div>
124
+ </>
125
+ );
126
+ };
127
+
128
+ export default ImagePicker;
@@ -0,0 +1,71 @@
1
+ import React, { useState } from "react";
2
+ import { Icon } from "@iconify/react";
3
+ import InputLabel from "./InputLabel";
4
+
5
+ const Input = React.forwardRef(
6
+ (
7
+ {
8
+ label,
9
+ required,
10
+ parentClass = "",
11
+ className = "",
12
+ type = "text",
13
+ onKeyDown,
14
+ ...props
15
+ },
16
+ ref,
17
+ ) => {
18
+ const [showPassword, setShowPassword] = useState(false);
19
+
20
+ const handleKeyDown = (e) => {
21
+ if (type === "number" && ["e", "E"].includes(e.key)) {
22
+ e.preventDefault();
23
+ }
24
+ onKeyDown?.(e);
25
+ };
26
+
27
+ const combinedClassName = `
28
+ h-10 placeholder-gray-400 dark:placeholder-gray-400
29
+ ${type === "password" ? "pr-10" : ""}
30
+ ${className}
31
+ `.trim();
32
+
33
+ return (
34
+ <>
35
+ <div key={props.name} className={parentClass || "col-span-12"}>
36
+ <InputLabel label={label} required={required} />
37
+ <div className="relative">
38
+ <input
39
+ type={type === "password" && showPassword ? "text" : type}
40
+ ref={ref}
41
+ required={required}
42
+ onKeyDown={handleKeyDown}
43
+ className={combinedClassName}
44
+ {...props}
45
+ />
46
+
47
+ {type === "password" && (
48
+ <button
49
+ type="button"
50
+ tabIndex={-1}
51
+ onClick={() => setShowPassword((prev) => !prev)}
52
+ className="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-200"
53
+ >
54
+ <Icon
55
+ icon={
56
+ showPassword ? "mdi:eye-off-outline" : "mdi:eye-outline"
57
+ }
58
+ className="w-5 h-5"
59
+ />
60
+ </button>
61
+ )}
62
+ </div>
63
+ </div>
64
+ </>
65
+ );
66
+ },
67
+ );
68
+
69
+ Input.displayName = "Input";
70
+
71
+ export { Input };
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export default function InputLabel({ label, required = false }) {
4
+ return (
5
+ <>
6
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
7
+ {label}
8
+ {required && <span className="ml-1">*</span>}
9
+ </label>
10
+ </>
11
+ );
12
+ }
@@ -0,0 +1,221 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+ import { countries } from "../../../data/countries";
3
+ import { ChevronDown, Search } from "lucide-react";
4
+ import InputLabel from "./InputLabel";
5
+
6
+ export default function PhoneInput({
7
+ label = "",
8
+ value = "",
9
+ name = "",
10
+ parentClass = "",
11
+ onChange,
12
+ disabled = false,
13
+ required = false,
14
+ placeholder = "Phone number",
15
+ search = false,
16
+ countriesList = false,
17
+ defaultCountry = "",
18
+ }) {
19
+ const find_country_with_code = (value) => {
20
+ return countries.find((obj) => obj.code == value);
21
+ };
22
+
23
+ const [selectedCountry, setSelectedCountry] = useState(
24
+ find_country_with_code(defaultCountry) || countries[0],
25
+ );
26
+ const [fullNumber, setFullNumber] = useState("");
27
+ const [open, setOpen] = useState(false);
28
+ const [searchTerm, setSearchTerm] = useState("");
29
+ const dropdownRef = useRef();
30
+
31
+ // Match prefix when value starts with +code
32
+ useEffect(() => {
33
+ if (typeof value === "string" && value.startsWith("+")) {
34
+ const match = countries
35
+ .filter((c) => value.startsWith("+" + c.phone))
36
+ .sort((a, b) => b.phone.length - a.phone.length)[0];
37
+ if (match) {
38
+ setSelectedCountry(match);
39
+ setFullNumber(value.replace("+" + match.phone, ""));
40
+ return;
41
+ }
42
+ }
43
+ setFullNumber(value);
44
+ }, [value]);
45
+
46
+ const handleNumberChange = (e) => {
47
+ const input = e.target.value.replace(/\D/g, "");
48
+ setFullNumber(input);
49
+ if (selectedCountry && onChange) {
50
+ onChange("+" + selectedCountry.phone + input);
51
+ }
52
+ };
53
+
54
+ const handleCountrySelect = (country) => {
55
+ setSelectedCountry(country);
56
+ if (onChange) onChange("+" + country.phone + fullNumber);
57
+ setOpen(false);
58
+ setSearchTerm("");
59
+ };
60
+
61
+ // 🖱️ Close dropdown on outside click
62
+ useEffect(() => {
63
+ const handleClickOutside = (e) => {
64
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target))
65
+ setOpen(false);
66
+ };
67
+ document.addEventListener("mousedown", handleClickOutside);
68
+ return () => document.removeEventListener("mousedown", handleClickOutside);
69
+ }, []);
70
+
71
+ const filteredCountries = countries.filter(
72
+ (c) =>
73
+ c.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
74
+ c.phone.includes(searchTerm),
75
+ );
76
+
77
+ if (!countriesList) {
78
+ const handleInputChange = (e) => {
79
+ const input = e.target.value.replace(/[^+\d]/g, "");
80
+ const formatted = input.startsWith("+")
81
+ ? "+" + input.replace(/[+]/g, "").slice(0)
82
+ : input;
83
+ onChange(formatted);
84
+ };
85
+
86
+ return (
87
+ <>
88
+ <div key={name} className={parentClass || "col-span-12"}>
89
+ <InputLabel label={label} required={required} />
90
+ <input
91
+ type="text"
92
+ value={value}
93
+ onChange={handleInputChange}
94
+ placeholder={placeholder}
95
+ disabled={disabled}
96
+ required={required}
97
+ className="w-full h-10 px-3 text-sm border border-gray-300 dark:border-gray-600 rounded-md
98
+ bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none
99
+ focus:ring-1 focus:ring-blue-300 dark:focus:ring-blue-200"
100
+ inputMode="tel"
101
+ pattern="^\+\d{1,15}$"
102
+ />
103
+ </div>
104
+ </>
105
+ );
106
+ }
107
+
108
+ return (
109
+ <>
110
+ <div key={name} className={parentClass || "col-span-12"}>
111
+ <InputLabel label={label} required={required} />
112
+ <div className="relative " ref={dropdownRef}>
113
+ {/* Input container */}
114
+ <div
115
+ className={`h-[40px] flex items-center border rounded-md px-2 bg-white dark:bg-gray-700 transition-all
116
+ ${
117
+ open
118
+ ? "ring-0.5 ring-blue-100 border-blue-300"
119
+ : "border-gray-300 dark:border-gray-600"
120
+ }
121
+ ${disabled ? "opacity-60 cursor-not-allowed" : ""}`}
122
+ >
123
+ {/* Flag & dropdown */}
124
+ <button
125
+ type="button"
126
+ disabled={disabled}
127
+ onClick={() => setOpen(!open)}
128
+ className="flex items-center gap-1 pr-2 border-r border-gray-300 dark:border-gray-700 focus:outline-none"
129
+ >
130
+ {selectedCountry ? (
131
+ <img
132
+ src={`https://flagcdn.com/w20/${selectedCountry.code.toLowerCase()}.png`}
133
+ alt={selectedCountry.code}
134
+ className="w-5 h-3 object-cover"
135
+ />
136
+ ) : (
137
+ <span className="text-gray-400 text-xs">🌐</span>
138
+ )}
139
+ <ChevronDown className="w-3 h-3 text-gray-500" />
140
+ </button>
141
+
142
+ {/* Country code */}
143
+ {selectedCountry && (
144
+ <span className="ml-2 text-sm text-gray-700 dark:text-gray-200 whitespace-nowrap">
145
+ +{selectedCountry.phone}
146
+ </span>
147
+ )}
148
+
149
+ {/* Number input */}
150
+ <input
151
+ type="tel"
152
+ value={fullNumber}
153
+ onChange={handleNumberChange}
154
+ required={required}
155
+ disabled={disabled || !selectedCountry}
156
+ placeholder={!selectedCountry ? "Select a country" : placeholder}
157
+ className="flex-1 ml-2 bg-transparent outline-none text-sm text-gray-800 dark:text-gray-100 placeholder-gray-400"
158
+ />
159
+
160
+ {/* Hidden input for native browser validation */}
161
+ <input
162
+ type="tel"
163
+ required={required}
164
+ tabIndex={-1}
165
+ readOnly
166
+ value={
167
+ selectedCountry && fullNumber
168
+ ? "+" + selectedCountry.phone + fullNumber
169
+ : ""
170
+ }
171
+ style={{
172
+ position: "absolute",
173
+ opacity: 0,
174
+ pointerEvents: "none",
175
+ height: 0,
176
+ }}
177
+ />
178
+ </div>
179
+
180
+ {/* Dropdown */}
181
+ {open && (
182
+ <div className="absolute top-full left-0 w-full mt-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-700 shadow-lg z-50 max-h-60 overflow-y-auto">
183
+ {search && (
184
+ <div className="p-2 border-b border-gray-200 dark:border-gray-700">
185
+ <div className="relative">
186
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
187
+ <input
188
+ type="text"
189
+ value={searchTerm}
190
+ onChange={(e) => setSearchTerm(e.target.value)}
191
+ placeholder="Search country..."
192
+ 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 "
193
+ />
194
+ </div>
195
+ </div>
196
+ )}
197
+
198
+ {filteredCountries.map((c) => (
199
+ <button
200
+ key={c.code}
201
+ type="button"
202
+ onClick={() => handleCountrySelect(c)}
203
+ className="w-full flex items-center gap-2 px-2 py-1 text-sm hover:bg-blue-50 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100"
204
+ >
205
+ <img
206
+ src={`https://flagcdn.com/w20/${c.code.toLowerCase()}.png`}
207
+ alt={c.code}
208
+ className="w-5 h-3 object-cover"
209
+ />
210
+ <span>
211
+ {c.label} (+{c.phone})
212
+ </span>
213
+ </button>
214
+ ))}
215
+ </div>
216
+ )}
217
+ </div>
218
+ </div>
219
+ </>
220
+ );
221
+ }