@jsenv/navi 0.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 (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,415 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:," />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Resource Demo 3 - Filtered Users</title>
8
+ <style>
9
+ body {
10
+ font-family: Arial, sans-serif;
11
+ padding: 20px;
12
+ }
13
+ .filters {
14
+ background: #f5f5f5;
15
+ padding: 15px;
16
+ margin-bottom: 20px;
17
+ border-radius: 5px;
18
+ }
19
+ .user-list {
20
+ margin: 20px 0;
21
+ }
22
+ .user-item {
23
+ padding: 10px;
24
+ margin: 5px 0;
25
+ border: 1px solid #ddd;
26
+ border-radius: 3px;
27
+ background: white;
28
+ }
29
+ .user-item.loaded {
30
+ border-color: #28a745;
31
+ background: #f8fff8;
32
+ }
33
+ .button-group {
34
+ margin: 10px 0;
35
+ }
36
+ button {
37
+ margin: 2px;
38
+ padding: 5px 10px;
39
+ }
40
+ .create-user {
41
+ background: #e9ecef;
42
+ padding: 15px;
43
+ margin: 20px 0;
44
+ border-radius: 5px;
45
+ }
46
+ input,
47
+ select {
48
+ margin: 5px;
49
+ padding: 5px;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <div id="root"></div>
55
+
56
+ <script type="module" jsenv-type="module/jsx">
57
+ import { render } from "preact";
58
+ import { signal } from "@preact/signals";
59
+ import { useState } from "preact/hooks";
60
+ import {
61
+ useActionStatus,
62
+ resource,
63
+ // eslint-disable-next-line no-unused-vars
64
+ ActionRenderer,
65
+ } from "@jsenv/navi";
66
+ import {
67
+ // eslint-disable-next-line no-unused-vars
68
+ ActionStatus,
69
+ } from "./action_status.jsx";
70
+
71
+ // Base de données simulée
72
+ const allUsers = [
73
+ {
74
+ id: 1,
75
+ name: "Alice",
76
+ gender: "female",
77
+ age: 25,
78
+ department: "Engineering",
79
+ },
80
+ {
81
+ id: 2,
82
+ name: "Bob",
83
+ gender: "male",
84
+ age: 30,
85
+ department: "Marketing",
86
+ },
87
+ {
88
+ id: 3,
89
+ name: "Charlie",
90
+ gender: "male",
91
+ age: 35,
92
+ department: "Engineering",
93
+ },
94
+ {
95
+ id: 4,
96
+ name: "Diana",
97
+ gender: "female",
98
+ age: 28,
99
+ department: "Sales",
100
+ },
101
+ {
102
+ id: 5,
103
+ name: "Eve",
104
+ gender: "female",
105
+ age: 32,
106
+ department: "Marketing",
107
+ },
108
+ ];
109
+ const USER = resource("user", {
110
+ idKey: "id",
111
+ mutableIdKey: "name",
112
+ GET_MANY: async (filters) => {
113
+ console.log("Loading users", filters);
114
+ await new Promise((resolve) => setTimeout(resolve, 500));
115
+ return allUsers.filter((user) => {
116
+ if (
117
+ filters.gender !== undefined &&
118
+ user.gender !== filters.gender
119
+ ) {
120
+ return false;
121
+ }
122
+ if (
123
+ filters.department !== undefined &&
124
+ user.department !== filters.department
125
+ ) {
126
+ return false;
127
+ }
128
+ return true;
129
+ });
130
+ },
131
+ GET: async ({ id }) => {
132
+ await new Promise((resolve) => setTimeout(resolve, 300));
133
+ const foundUser = allUsers.find((u) => u.id === id);
134
+ if (!foundUser) {
135
+ throw new Error(`User ${id} not found`);
136
+ }
137
+ return {
138
+ ...foundUser,
139
+ loadedAt: new Date().toLocaleTimeString(),
140
+ };
141
+ },
142
+ POST: async (newUser) => {
143
+ await new Promise((resolve) => setTimeout(resolve, 500));
144
+ return newUser;
145
+ },
146
+ PUT: async ({ id, field, value }) => {
147
+ await new Promise((resolve) => setTimeout(resolve, 500));
148
+ return { id, [field]: value };
149
+ },
150
+ });
151
+ USER.RENAME = USER.PUT.bindParams({
152
+ field: "name",
153
+ });
154
+ // Signal pour les filtres
155
+ const filtersSignal = signal({
156
+ gender: undefined,
157
+ department: undefined,
158
+ });
159
+ USER.GET_MANY_FILTERED = USER.GET_MANY.bindParams(filtersSignal);
160
+ USER.GET_MANY_FILTERED.load();
161
+ USER.store.observeProperties((mutations) => {
162
+ const nameMutation = mutations.name;
163
+ if (nameMutation) {
164
+ const { target, oldValue, newValue } = nameMutation;
165
+ console.log(
166
+ `user ${target.id} name changed from "${oldValue}" to "${newValue}"`,
167
+ );
168
+ }
169
+ });
170
+
171
+ // eslint-disable-next-line no-unused-vars
172
+ const App = () => {
173
+ const { data: serverUsers, pending: loadingUsers } = useActionStatus(
174
+ USER.GET_MANY_FILTERED,
175
+ );
176
+
177
+ const [newUserName, setNewUserName] = useState("");
178
+ const [newUserGender, setNewUserGender] = useState("male");
179
+ const [newUserAge, setNewUserAge] = useState(25);
180
+ const [newUserDept, setNewUserDept] = useState("Engineering");
181
+
182
+ const createUser = () => {
183
+ if (!newUserName.trim()) {
184
+ return;
185
+ }
186
+ const newUser = {
187
+ id: Math.max(...allUsers.map((u) => u.id)) + 1,
188
+ name: newUserName,
189
+ gender: newUserGender,
190
+ age: parseInt(newUserAge),
191
+ department: newUserDept,
192
+ };
193
+ setNewUserName("");
194
+ allUsers.push(newUser);
195
+ const createUser = USER.POST.bindParams(newUser);
196
+ createUser.load();
197
+ };
198
+
199
+ return (
200
+ <div>
201
+ <h1>Users Management Demo</h1>
202
+
203
+ <button
204
+ onClick={() => {
205
+ USER.GET_MANY_FILTERED.reload();
206
+ }}
207
+ >
208
+ Reload
209
+ </button>
210
+
211
+ <div className="filters">
212
+ <h3>Filters</h3>
213
+ <fieldset>
214
+ <legend>Gender</legend>
215
+
216
+ <label>
217
+ All:
218
+ <input
219
+ name="gender"
220
+ type="radio"
221
+ value="all"
222
+ checked={filtersSignal.value.gender === undefined}
223
+ onChange={(e) => {
224
+ if (e.target.checked) {
225
+ filtersSignal.value = {
226
+ ...filtersSignal.value,
227
+ gender: undefined,
228
+ };
229
+ }
230
+ }}
231
+ />
232
+ </label>
233
+
234
+ <label>
235
+ Male:
236
+ <input
237
+ name="gender"
238
+ type="radio"
239
+ value="male"
240
+ checked={filtersSignal.value.gender === "male"}
241
+ onChange={(e) => {
242
+ if (e.target.checked) {
243
+ filtersSignal.value = {
244
+ ...filtersSignal.value,
245
+ gender: e.target.value,
246
+ };
247
+ }
248
+ }}
249
+ />
250
+ </label>
251
+
252
+ <label>
253
+ Female:
254
+ <input
255
+ name="gender"
256
+ type="radio"
257
+ value="female"
258
+ checked={filtersSignal.value.gender === "female"}
259
+ onChange={(e) => {
260
+ if (e.target.checked) {
261
+ filtersSignal.value = {
262
+ ...filtersSignal.value,
263
+ gender: e.target.value,
264
+ };
265
+ }
266
+ }}
267
+ />
268
+ </label>
269
+ </fieldset>
270
+
271
+ <label>
272
+ Department:
273
+ <select
274
+ value={filtersSignal.value.department}
275
+ onChange={(e) => {
276
+ filtersSignal.value = {
277
+ ...filtersSignal.value,
278
+ department: e.target.value,
279
+ };
280
+ }}
281
+ >
282
+ <option value="all">All</option>
283
+ <option value="Engineering">Engineering</option>
284
+ <option value="Marketing">Marketing</option>
285
+ <option value="Sales">Sales</option>
286
+ </select>
287
+ </label>
288
+ </div>
289
+
290
+ {/* Liste des utilisateurs du serveur - LA SEULE LISTE */}
291
+ <div className="user-list">
292
+ <h3>
293
+ Filtered Users from Server {loadingUsers && "- Loading..."}
294
+ </h3>
295
+ <p>Count: {serverUsers.length}</p>
296
+ {serverUsers.map((user) => {
297
+ return (
298
+ <UserItem
299
+ key={user}
300
+ user={user}
301
+ onUpdateName={async (name) => {
302
+ const renameAction = USER.RENAME.bindParams({
303
+ id: user.id,
304
+ value: name,
305
+ });
306
+ await renameAction.load();
307
+ const harcodedUser = allUsers.find(
308
+ (u) => u.id === user.id,
309
+ );
310
+ harcodedUser.name = name;
311
+ }}
312
+ />
313
+ );
314
+ })}
315
+ </div>
316
+
317
+ {/* Création d'utilisateur */}
318
+ <div className="create-user">
319
+ <h3>Create New User</h3>
320
+ <p>
321
+ <em>
322
+ Note: Les nouveaux utilisateurs n'apparaîtront que s'ils
323
+ matchent les filtres actuels
324
+ </em>
325
+ </p>
326
+ <input
327
+ type="text"
328
+ placeholder="Name"
329
+ value={newUserName}
330
+ onChange={(e) => setNewUserName(e.target.value)}
331
+ />
332
+ <select
333
+ value={newUserGender}
334
+ onChange={(e) => setNewUserGender(e.target.value)}
335
+ >
336
+ <option value="male">Male</option>
337
+ <option value="female">Female</option>
338
+ </select>
339
+ <input
340
+ type="number"
341
+ placeholder="Age"
342
+ value={newUserAge}
343
+ onChange={(e) => setNewUserAge(e.target.value)}
344
+ />
345
+ <select
346
+ value={newUserDept}
347
+ onChange={(e) => setNewUserDept(e.target.value)}
348
+ >
349
+ <option value="Engineering">Engineering</option>
350
+ <option value="Marketing">Marketing</option>
351
+ <option value="Sales">Sales</option>
352
+ </select>
353
+ <button onClick={createUser}>Create User</button>
354
+ </div>
355
+
356
+ <ActionStatus action={USER.GET_MANY_FILTERED} />
357
+ </div>
358
+ );
359
+ };
360
+
361
+ // eslint-disable-next-line no-unused-vars
362
+ const UserItem = ({ user, onUpdateName }) => {
363
+ const userAction = USER.GET.bindParams({
364
+ id: user.id,
365
+ });
366
+ const { pending, loaded } = useActionStatus(userAction);
367
+ const [editingName, setEditingName] = useState(false);
368
+ const [newName, setNewName] = useState(user.name);
369
+
370
+ const handleNameUpdate = async () => {
371
+ await onUpdateName(newName);
372
+ setEditingName(false);
373
+ };
374
+
375
+ return (
376
+ <div className={`user-item ${loaded ? "loaded" : ""}`}>
377
+ <div>
378
+ <strong>
379
+ {editingName ? (
380
+ <span>
381
+ <input
382
+ value={newName}
383
+ onChange={(e) => setNewName(e.target.value)}
384
+ onKeyDown={(e) => e.key === "Enter" && handleNameUpdate()}
385
+ />
386
+ <button onClick={handleNameUpdate}>✓</button>
387
+ <button onClick={() => setEditingName(false)}>✗</button>
388
+ </span>
389
+ ) : (
390
+ <span
391
+ onClick={() => setEditingName(true)}
392
+ style={{ cursor: "pointer" }}
393
+ >
394
+ {user.name}
395
+ {user.loadedAt && ` (loaded at ${user.loadedAt})`}
396
+ </span>
397
+ )}
398
+ </strong>
399
+ - {user.gender}, {user.age}y, {user.department}
400
+ </div>
401
+
402
+ <div className="button-group">
403
+ <button onClick={() => userAction.reload()}>Reload</button>
404
+ <button onClick={() => userAction.unload()}>Unload</button>
405
+ <button onClick={() => userAction.preload()}>Preload</button>
406
+ {pending && <span>Loading...</span>}
407
+ </div>
408
+ </div>
409
+ );
410
+ };
411
+
412
+ render(<App />, document.querySelector("#root"));
413
+ </script>
414
+ </body>
415
+ </html>
@@ -0,0 +1,284 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:," />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Resource demo - Details with localStorage</title>
8
+ </head>
9
+ <body>
10
+ <div
11
+ id="root"
12
+ style="position: relative; width: 400px; padding: 20px"
13
+ ></div>
14
+
15
+ <script type="module" jsenv-type="module/jsx">
16
+ import { render } from "preact";
17
+ import { useState, useEffect } from "preact/hooks";
18
+ import {
19
+ createAction,
20
+ useActionStatus,
21
+ // eslint-disable-next-line no-unused-vars
22
+ ActionRenderer,
23
+ } from "@jsenv/navi";
24
+
25
+ // Mock API calls
26
+ const fetchUser = async ({ userId }) => {
27
+ await new Promise((resolve) => setTimeout(resolve, 800));
28
+ return {
29
+ id: userId,
30
+ name: `User ${userId}`,
31
+ email: `user${userId}@example.com`,
32
+ };
33
+ };
34
+ const fetchUserFriends = async ({ userId }, { signal }) => {
35
+ console.log(`Fetching friends for user ${userId}`);
36
+
37
+ await new Promise((resolve) => {
38
+ const timeout = setTimeout(resolve, 1200);
39
+ signal.addEventListener("abort", () => {
40
+ clearTimeout(timeout);
41
+ });
42
+ });
43
+ return [
44
+ { id: 1, name: "Alice Johnson" },
45
+ { id: 2, name: "Bob Smith" },
46
+ { id: 3, name: "Carol Davis" },
47
+ ];
48
+ };
49
+ const fetchUserSettings = async ({ userId }) => {
50
+ console.log(`Fetching settings for user ${userId}`);
51
+ await new Promise((resolve) => setTimeout(resolve, 600));
52
+ return {
53
+ theme: "dark",
54
+ notifications: true,
55
+ privacy: "friends-only",
56
+ };
57
+ };
58
+
59
+ // Action templates
60
+ const GET_USER = createAction(fetchUser, {
61
+ params: {
62
+ name: "getUser",
63
+ },
64
+ });
65
+
66
+ const GET_USER_FRIENDS = createAction(fetchUserFriends, {
67
+ params: {
68
+ name: "getUserFriends",
69
+ },
70
+ });
71
+
72
+ const GET_USER_SETTINGS = createAction(fetchUserSettings, {
73
+ params: {
74
+ name: "getUserSettings",
75
+ },
76
+ });
77
+
78
+ // Hook pour gérer l'état des details avec localStorage
79
+ const useToggleableAction = (action, storageKey) => {
80
+ const [isOpen, setIsOpen] = useState(() => {
81
+ return localStorage.getItem(storageKey) !== null;
82
+ });
83
+
84
+ const handleToggle = (e) => {
85
+ const open = e.target.open;
86
+ setIsOpen(open);
87
+ if (open) {
88
+ action.load();
89
+ localStorage.setItem(storageKey, "1");
90
+ } else {
91
+ action.abort();
92
+ localStorage.removeItem(storageKey);
93
+ }
94
+ };
95
+
96
+ // Charger au montage si c'était ouvert
97
+ useEffect(() => {
98
+ if (isOpen) {
99
+ action.load();
100
+ }
101
+ }, []);
102
+
103
+ return [isOpen, handleToggle];
104
+ };
105
+
106
+ // Composant principal
107
+ // eslint-disable-next-line no-unused-vars
108
+ const UserProfile = ({ userId = 123 }) => {
109
+ const getUser = GET_USER.bindParams({ userId });
110
+ const getUserFriends = GET_USER_FRIENDS.bindParams({ userId });
111
+ const getUserSettings = GET_USER_SETTINGS.bindParams({ userId });
112
+
113
+ const [friendsOpen, handleFriendsToggle] = useToggleableAction(
114
+ getUserFriends,
115
+ `user_${userId}_friends_open`,
116
+ );
117
+
118
+ const { data: friends } = useActionStatus(getUserFriends);
119
+
120
+ const [settingsOpen, handleSettingsToggle] = useToggleableAction(
121
+ getUserSettings,
122
+ `user_${userId}_settings_open`,
123
+ );
124
+
125
+ // Charger les données de base de l'utilisateur
126
+ useEffect(() => {
127
+ getUser.load();
128
+ }, []);
129
+
130
+ return (
131
+ <div style={{ fontFamily: "system-ui", lineHeight: "1.5" }}>
132
+ <h2 style={{ margin: "0 0 20px 0", color: "#333" }}>
133
+ 👤 User Profile
134
+ </h2>
135
+
136
+ {/* Données de base de l'utilisateur */}
137
+ <div style={{ marginBottom: "20px" }}>
138
+ <ActionRenderer action={getUser}>
139
+ {(user) => (
140
+ <div
141
+ style={{
142
+ padding: "12px",
143
+ backgroundColor: "#e3f2fd",
144
+ borderRadius: "4px",
145
+ }}
146
+ >
147
+ <strong>{user.name}</strong>
148
+ <br />
149
+ 📧 {user.email}
150
+ </div>
151
+ )}
152
+ </ActionRenderer>
153
+ </div>
154
+
155
+ {/* Details Friends */}
156
+ <details
157
+ open={friendsOpen}
158
+ onToggle={handleFriendsToggle}
159
+ style={{ marginBottom: "16px" }}
160
+ >
161
+ <summary
162
+ style={{
163
+ cursor: "pointer",
164
+ display: "flex",
165
+ padding: "8px",
166
+ backgroundColor: "#fff3e0",
167
+ borderRadius: "4px",
168
+ fontWeight: "bold",
169
+ }}
170
+ >
171
+ 👥 Friends {friends ? `(${friends.length})` : ""}
172
+ <span style="flex:1"></span>
173
+ <button
174
+ onClick={() => {
175
+ getUserFriends.reload();
176
+ }}
177
+ >
178
+ Refresh
179
+ </button>
180
+ </summary>
181
+ <div style={{ padding: "12px", borderLeft: "3px solid #ff9800" }}>
182
+ <ActionRenderer action={getUserFriends}>
183
+ {(friends) => (
184
+ <ul style={{ margin: 0, paddingLeft: "20px" }}>
185
+ {friends.map((friend) => (
186
+ <li key={friend.id}>
187
+ <strong>{friend.name}</strong>
188
+ </li>
189
+ ))}
190
+ </ul>
191
+ )}
192
+ </ActionRenderer>
193
+ </div>
194
+ </details>
195
+
196
+ {/* Details Settings */}
197
+ <details
198
+ open={settingsOpen}
199
+ onToggle={handleSettingsToggle}
200
+ style={{ marginBottom: "16px" }}
201
+ >
202
+ <summary
203
+ style={{
204
+ cursor: "pointer",
205
+ padding: "8px",
206
+ backgroundColor: "#f3e5f5",
207
+ borderRadius: "4px",
208
+ fontWeight: "bold",
209
+ }}
210
+ >
211
+ ⚙️ Settings
212
+ </summary>
213
+ <div style={{ padding: "12px", borderLeft: "3px solid #9c27b0" }}>
214
+ <ActionRenderer action={getUserSettings}>
215
+ {(settings) => (
216
+ <div>
217
+ <div>
218
+ <strong>Theme:</strong> {settings.theme}
219
+ </div>
220
+ <div>
221
+ <strong>Notifications:</strong>{" "}
222
+ {settings.notifications ? "On" : "Off"}
223
+ </div>
224
+ <div>
225
+ <strong>Privacy:</strong> {settings.privacy}
226
+ </div>
227
+ </div>
228
+ )}
229
+ </ActionRenderer>
230
+ </div>
231
+ </details>
232
+
233
+ {/* Debug info */}
234
+ <div
235
+ style={{
236
+ marginTop: "20px",
237
+ padding: "12px",
238
+ backgroundColor: "#f8f9fa",
239
+ borderRadius: "4px",
240
+ fontSize: "12px",
241
+ color: "#666",
242
+ }}
243
+ >
244
+ <strong>Debug Info:</strong>
245
+ <br />
246
+ Friends Details: {friendsOpen ? "Open" : "Closed"}
247
+ <br />
248
+ Settings Details: {settingsOpen ? "Open" : "Closed"}
249
+ <br />
250
+ localStorage: Check DevTools → Application → Local Storage
251
+ </div>
252
+
253
+ {/* Actions de test */}
254
+ <div style={{ marginTop: "16px" }}>
255
+ <button
256
+ onClick={() => {
257
+ try {
258
+ localStorage.clear();
259
+ window.location.reload();
260
+ } catch (error) {
261
+ console.warn("Failed to clear localStorage:", error);
262
+ }
263
+ }}
264
+ style={{
265
+ padding: "8px 16px",
266
+ backgroundColor: "#dc3545",
267
+ color: "white",
268
+ border: "none",
269
+ borderRadius: "4px",
270
+ cursor: "pointer",
271
+ }}
272
+ >
273
+ 🗑️ Clear localStorage & Reload
274
+ </button>
275
+ </div>
276
+ </div>
277
+ );
278
+ };
279
+
280
+ // Render
281
+ render(<UserProfile userId={123} />, document.getElementById("root"));
282
+ </script>
283
+ </body>
284
+ </html>