@airoom/nextmin-react 0.1.4 → 0.1.5

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.
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Autocomplete, AutocompleteItem } from '@heroui/react';
5
5
  import { loadGoogleMaps } from '../lib/googleLoader';
@@ -13,7 +13,7 @@ function deriveStableKey(s) {
13
13
  return pid ? String(pid) : label ? String(label) : null;
14
14
  }
15
15
  export default function AddressAutocompleteGoogle({ name, label, description, value, onChange, disabled, required, className, classNames, countryCodes, limit = 8, apiKey, }) {
16
- // Controlled input
16
+ // ---------------- Hooks (ALL before any return) ----------------
17
17
  const [q, setQ] = useState(value ?? '');
18
18
  useEffect(() => {
19
19
  if ((value ?? '') !== q)
@@ -22,41 +22,45 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
22
22
  }, [value]);
23
23
  const [items, setItems] = useState([]);
24
24
  const [keyMissing, setKeyMissing] = useState(false);
25
- // Google + guards
26
25
  const tokenRef = useRef(null);
27
26
  const suppressFetchRef = useRef(false);
28
27
  const fetchGenRef = useRef(0);
29
- // Clears
30
28
  const manualClearRef = useRef(false);
31
29
  const locationRestriction = useMemo(() => {
32
30
  const cc = (countryCodes?.[0] ?? '').toLowerCase();
33
- if (cc === 'bd' || !cc)
31
+ if (cc === 'bd' || !cc) {
34
32
  return { west: 88.008, south: 20.67, east: 92.68, north: 26.634 };
33
+ }
35
34
  return undefined;
36
35
  }, [countryCodes]);
37
- // Load Google once (and on key change)
36
+ // Compute readiness; DON'T early-return before hooks are declared
37
+ const ready = Boolean(apiKey && apiKey.trim()) && !keyMissing;
38
+ // Load Google once (and when key toggles)
38
39
  useEffect(() => {
40
+ if (!apiKey || !apiKey.trim()) {
41
+ setKeyMissing(true);
42
+ tokenRef.current = null;
43
+ return;
44
+ }
39
45
  (async () => {
40
46
  try {
41
- const g = await loadGoogleMaps(apiKey);
47
+ const g = await loadGoogleMaps(apiKey); // must include libraries=places
42
48
  tokenRef.current = new g.maps.places.AutocompleteSessionToken();
43
49
  setKeyMissing(false);
44
50
  }
45
51
  catch {
46
52
  setKeyMissing(true);
53
+ tokenRef.current = null;
47
54
  }
48
55
  })();
49
56
  }, [apiKey]);
50
- if (!apiKey || !apiKey.trim()) {
51
- return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing. Set ", _jsx("b", { children: "system.googleMapsKey" }), " or define ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), "."] }));
52
- }
53
- if (keyMissing) {
54
- return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing or invalid. Set", ' ', _jsx("b", { children: "system.googleMapsKey" }), " or ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), "."] }));
55
- }
56
- // Debounced suggestions fetch (Places API New)
57
+ // Debounced suggestions fetch (always declared, internally gated by `ready`)
57
58
  useEffect(() => {
58
- if (suppressFetchRef.current)
59
+ if (!ready || suppressFetchRef.current) {
60
+ if (!ready)
61
+ setItems([]);
59
62
  return;
63
+ }
60
64
  const trimmed = (q ?? '').trim();
61
65
  if (!trimmed || trimmed.length < 3) {
62
66
  setItems([]);
@@ -84,8 +88,8 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
84
88
  .slice(0, Math.max(1, limit))
85
89
  .map((s) => {
86
90
  const key = deriveStableKey(s);
87
- const label = s.placePrediction?.text?.toString?.() ?? '';
88
- return key && label ? { key, label, raw: s } : null;
91
+ const lbl = s.placePrediction?.text?.toString?.() ?? '';
92
+ return key && lbl ? { key, label: lbl, raw: s } : null;
89
93
  })
90
94
  .filter((r) => !!r);
91
95
  setItems(mapped);
@@ -99,8 +103,9 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
99
103
  cancelled = true;
100
104
  clearTimeout(t);
101
105
  };
102
- // eslint-disable-next-line react-hooks/exhaustive-deps
103
- }, [q, apiKey, countryCodes, locationRestriction, limit]);
106
+ }, [ready, q, apiKey, countryCodes, locationRestriction, limit]);
107
+ // ----------------------------------------------------------------
108
+ // Utilities (no hooks)
104
109
  const getLatLng = (loc) => {
105
110
  if (!loc)
106
111
  return null;
@@ -110,15 +115,13 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
110
115
  return { lat: loc.lat, lng: loc.lng };
111
116
  return null;
112
117
  };
113
- // Resolve and commit
114
118
  const resolveByKey = async (key) => {
115
119
  const rec = items.find((r) => r.key === key);
116
120
  if (!rec)
117
121
  return;
118
122
  suppressFetchRef.current = true;
119
- // Show EXACT suggestion text immediately, and keep it
120
- const label = rec.label;
121
- setQ(label);
123
+ const labelText = rec.label;
124
+ setQ(labelText);
122
125
  setItems([]);
123
126
  try {
124
127
  const g = await loadGoogleMaps(apiKey);
@@ -127,20 +130,17 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
127
130
  return;
128
131
  const place = pp.toPlace();
129
132
  await place.fetchFields({
130
- fields: ['location'], // we only need lat/lng; do NOT fetch formattedAddress
133
+ fields: ['location'],
131
134
  sessionToken: tokenRef.current ?? undefined,
132
135
  });
133
136
  const ll = getLatLng(place.location);
134
- // Always commit the SUGGESTION TEXT to the form; populate latLng when available
135
- onChange(label, ll ?? undefined);
136
- // fresh session
137
+ onChange(labelText, ll ?? undefined);
137
138
  tokenRef.current = new g.maps.places.AutocompleteSessionToken();
138
139
  }
139
140
  finally {
140
141
  suppressFetchRef.current = false;
141
142
  }
142
143
  };
143
- // Normalize HeroUI selection payload
144
144
  function extractKey(sel) {
145
145
  if (sel === null || sel === undefined || sel === 'all')
146
146
  return null;
@@ -150,7 +150,8 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
150
150
  }
151
151
  return String(sel);
152
152
  }
153
- return (_jsx("div", { className: className, children: _jsx(Autocomplete, { "aria-label": label ?? name, label: label, labelPlacement: "outside", placeholder: "Type address", name: name, isRequired: required, description: description, variant: "bordered", classNames: classNames, isDisabled: disabled, items: items, inputValue: q, isClearable: true, onClear: () => {
153
+ // ------- Render: keep subtree shape stable; disable when not ready -------
154
+ return (_jsx("div", { className: className, children: _jsx(Autocomplete, { "aria-label": label ?? name, label: label, labelPlacement: "outside", placeholder: "Type address", name: name, isRequired: required, description: description, variant: "bordered", classNames: classNames, isDisabled: disabled || !ready, items: ready ? items : [], inputValue: q, isClearable: true, onClear: () => {
154
155
  manualClearRef.current = true;
155
156
  setQ('');
156
157
  setItems([]);
@@ -160,10 +161,11 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
160
161
  manualClearRef.current = true;
161
162
  }
162
163
  }, onInputChange: (text) => {
164
+ if (!ready)
165
+ return;
163
166
  if (text == null)
164
167
  return;
165
168
  const trimmed = text.trim();
166
- // Ignore blur-initiated clears unless it was a real manual clear
167
169
  if (trimmed === '' && !manualClearRef.current)
168
170
  return;
169
171
  if (trimmed === '' && manualClearRef.current) {
@@ -175,6 +177,8 @@ export default function AddressAutocompleteGoogle({ name, label, description, va
175
177
  if (text !== q)
176
178
  setQ(text);
177
179
  }, onSelectionChange: (sel) => {
180
+ if (!ready)
181
+ return;
178
182
  const key = extractKey(sel);
179
183
  if (key)
180
184
  void resolveByKey(key);
@@ -182,6 +182,7 @@ function normalizeTimeRangeLoose(value) {
182
182
  }
183
183
  /** --------------------------------------------------------------------------------------- **/
184
184
  export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, }) {
185
+ const mapsKey = useGoogleMapsKey();
185
186
  const formUid = useId();
186
187
  const { items } = useSelector((s) => s.schemas);
187
188
  const reduxUser = useSelector((s) => s?.session?.user ?? null) ??
@@ -294,10 +295,10 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
294
295
  ? form[name]
295
296
  : ''
296
297
  : (form[name] ?? (Array.isArray(attr) ? [] : ''));
297
- return (_jsx("div", { className: colClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, value: baseValue, onChange: handleChange, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames }) }, name));
298
+ return (_jsx("div", { className: colClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, value: baseValue, onChange: handleChange, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames, mapsKey: mapsKey }) }, name));
298
299
  }), error && (_jsx("div", { className: "col-span-2", children: _jsx("div", { className: "text-danger text-sm", children: error }) })), _jsxs("div", { className: "flex gap-2 col-span-2", children: [_jsx(Button, { type: "submit", color: "primary", isLoading: busy, children: submitLabel }), showReset && (_jsx(Button, { type: "reset", variant: "flat", isDisabled: busy, children: "Reset" }))] })] }));
299
300
  }
300
- function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, }) {
301
+ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, }) {
301
302
  const id = `${uid}-${name}`;
302
303
  const label = attr?.label ?? formatLabel(name);
303
304
  const required = !!attr?.required;
@@ -345,7 +346,6 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
345
346
  : '';
346
347
  return (_jsx(PhoneInput, { id: id, name: name, label: label, mask: phoneMask, value: rawDigits, onChange: (raw) => onChange(name, raw), disabled: disabled, required: required, description: description, className: "w-full", classNames: inputClassNames }));
347
348
  }
348
- const mapsKey = useGoogleMapsKey();
349
349
  const populateField = getPopulateTarget(attr);
350
350
  // Address → Autocomplete
351
351
  if (isAddressAttr(name, attr)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-react",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",