hazo_auth 0.1.0 → 0.3.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.
Files changed (194) hide show
  1. package/hazo_auth_config.example.ini +36 -0
  2. package/package.json +3 -1
  3. package/public/profile_pictures/library/Cars/001 - bugatti.jpeg +0 -0
  4. package/public/profile_pictures/library/Cars/002 - lamborgini.jpeg +0 -0
  5. package/public/profile_pictures/library/Cars/004 - chevrolet_silverado.jpeg +0 -0
  6. package/public/profile_pictures/library/Cars/007 - tesla_model_3.jpeg +0 -0
  7. package/public/profile_pictures/library/Cars/012 - subaru_outback.jpeg +0 -0
  8. package/public/profile_pictures/library/Cars/014 - ford_explorer.jpeg +0 -0
  9. package/public/profile_pictures/library/Cars/016 - gmc_sierra.jpeg +0 -0
  10. package/public/profile_pictures/library/Cars/021 - audi_a4.jpeg +0 -0
  11. package/public/profile_pictures/library/Cars/024 - audi_a6.jpeg +0 -0
  12. package/public/profile_pictures/library/Cars/025 - mercedes_benz_e_class.jpeg +0 -0
  13. package/public/profile_pictures/library/Cars/027 - porsche_911.jpeg +0 -0
  14. package/public/profile_pictures/library/Cars/031 - opel_corsa.jpeg +0 -0
  15. package/public/profile_pictures/library/Cars/036 - nissan_qashqai.jpeg +0 -0
  16. package/public/profile_pictures/library/Cars/037 - mini_hatch.jpeg +0 -0
  17. package/public/profile_pictures/library/Cars/040 - land_rover_range_rover_evoque.jpeg +0 -0
  18. package/public/profile_pictures/library/Cars/042 - land_rover_defender.jpeg +0 -0
  19. package/public/profile_pictures/library/Cars/050 - citroe/314/210n_c3.jpeg +0 -0
  20. package/public/profile_pictures/library/Cars/051 - renault_captur.jpeg +0 -0
  21. package/public/profile_pictures/library/Cars/059 - fiat_500.jpeg +0 -0
  22. package/public/profile_pictures/library/Cars/060 - lancia_ypsilon.jpeg +0 -0
  23. package/public/profile_pictures/library/Cars/062 - maserati_ghibli.jpeg +0 -0
  24. package/public/profile_pictures/library/Cars/063 - ferrari_488.jpeg +0 -0
  25. package/public/profile_pictures/library/Cars/064 - lamborghini_huraca/314/201n.jpeg +0 -0
  26. package/public/profile_pictures/library/Cars/065 - ferrari_sf90.jpeg +0 -0
  27. package/public/profile_pictures/library/Cars/066 - fiat_tipo.jpeg +0 -0
  28. package/public/profile_pictures/library/Cars/071 - seat_ateca.jpeg +0 -0
  29. package/public/profile_pictures/library/Cars/075 - hyundai_kona.jpeg +0 -0
  30. package/public/profile_pictures/library/Cars/078 - tesla_model_y.jpeg +0 -0
  31. package/public/profile_pictures/library/Cars/081 - bmw_i3.jpeg +0 -0
  32. package/public/profile_pictures/library/Cars/084 - fiat_500e.jpeg +0 -0
  33. package/public/profile_pictures/library/Cars/087 - polestar_2.jpeg +0 -0
  34. package/public/profile_pictures/library/Cars/088 - toyota_corolla.jpeg +0 -0
  35. package/public/profile_pictures/library/Cars/090 - bmw_x5.jpeg +0 -0
  36. package/public/profile_pictures/library/Cars/091 - ford_mustang.jpeg +0 -0
  37. package/public/profile_pictures/library/Cars/093 - audi_tt.jpeg +0 -0
  38. package/public/profile_pictures/library/Cars/094 - mercedes_benz_s_class.jpeg +0 -0
  39. package/public/profile_pictures/library/Cars/095 - volvo_xc60.jpeg +0 -0
  40. package/public/profile_pictures/library/Cars/097 - peugeot_5008.jpeg +0 -0
  41. package/public/profile_pictures/library/Cars/099 - citroe/314/210n_2cv_(classic).jpeg +0 -0
  42. package/public/profile_pictures/library/Cars/100 - vw_beetle_(classic).jpeg +0 -0
  43. package/public/profile_pictures/library/Cars/101 - range_rover_sport.jpeg +0 -0
  44. package/public/profile_pictures/library/Cars/104 - audi_a8.jpeg +0 -0
  45. package/public/profile_pictures/library/Cars/106 - mercedes_benz_g_class.jpeg +0 -0
  46. package/public/profile_pictures/library/Cars/110 - bmw_i7.jpeg +0 -0
  47. package/public/profile_pictures/library/Cars/111 - audi_e_tron_gt.jpeg +0 -0
  48. package/public/profile_pictures/library/Cars/112 - rolls_royce_phantom.jpeg +0 -0
  49. package/public/profile_pictures/library/Cars/113 - bentley_continental_gt.jpeg +0 -0
  50. package/public/profile_pictures/library/Cars/115 - aston_martin_dbx.jpeg +0 -0
  51. package/public/profile_pictures/library/Cars/116 - jaguar_xj.jpeg +0 -0
  52. package/public/profile_pictures/library/Cars/117 - maserati_quattroporte.jpeg +0 -0
  53. package/public/profile_pictures/library/Cars/118 - ferrari_roma.jpeg +0 -0
  54. package/public/profile_pictures/library/Cars/119 - lamborghini_urus.jpeg +0 -0
  55. package/public/profile_pictures/library/Cars/120 - cadillac_escalade.jpeg +0 -0
  56. package/public/profile_pictures/library/Cars/122 - ferrari_sf90_stradale.jpeg +0 -0
  57. package/public/profile_pictures/library/Cars/123 - lamborghini_aventador_svj.jpeg +0 -0
  58. package/public/profile_pictures/library/Cars/124 - mclaren_720s.jpeg +0 -0
  59. package/public/profile_pictures/library/Cars/125 - porsche_911_gt3_rs.jpeg +0 -0
  60. package/public/profile_pictures/library/Cars/126 - bugatti_chiron.jpeg +0 -0
  61. package/public/profile_pictures/library/Cars/127 - aston_martin_valkyrie.jpeg +0 -0
  62. package/public/profile_pictures/library/Cars/128 - koenigsegg_jesko.jpeg +0 -0
  63. package/public/profile_pictures/library/Cars/129 - pagani_huayra.jpeg +0 -0
  64. package/public/profile_pictures/library/Cars/130 - ferrari_812_superfast.jpeg +0 -0
  65. package/public/profile_pictures/library/Cars/131 - lamborghini_huraca/314/201n_sto.jpeg +0 -0
  66. package/public/profile_pictures/library/Cars/132 - mclaren_p1.jpeg +0 -0
  67. package/public/profile_pictures/library/Cars/133 - bugatti_veyron.jpeg +0 -0
  68. package/public/profile_pictures/library/Cars/134 - rimac_nevera.jpeg +0 -0
  69. package/public/profile_pictures/library/Cars/135 - maserati_mc20.jpeg +0 -0
  70. package/public/profile_pictures/library/Cars/136 - chevrolet_corvette_z06_(c8).jpeg +0 -0
  71. package/public/profile_pictures/library/Cars/137 - lotus_evija.jpeg +0 -0
  72. package/public/profile_pictures/library/Cars/138 - ford_gt.jpeg +0 -0
  73. package/public/profile_pictures/library/Cars/139 - ferrari_laferrari.jpeg +0 -0
  74. package/public/profile_pictures/library/Cars/140 - lamborghini_sian.jpeg +0 -0
  75. package/public/profile_pictures/library/Cars/141 - zenvo_tsr_s.jpeg +0 -0
  76. package/public/profile_pictures/library/Cars/142 - toyota_tacoma.jpeg +0 -0
  77. package/public/profile_pictures/library/Cars/143 - suzuki_swift.jpeg +0 -0
  78. package/public/profile_pictures/library/Cars/144 - skoda_fabia_.jpeg +0 -0
  79. package/public/profile_pictures/library/Cars/145 - nissan_micra.jpeg +0 -0
  80. package/public/profile_pictures/library/Cars/146 - chevrolet_equinox.jpeg +0 -0
  81. package/public/profile_pictures/library/Cars/147 - hyundai_elantra.jpeg +0 -0
  82. package/public/profile_pictures/library/Cars/148 - peugeot_301.jpeg +0 -0
  83. package/public/profile_pictures/library/Cars/149 - renault_duster.jpeg +0 -0
  84. package/public/profile_pictures/library/Cars/150 - tesla_roadster.jpeg +0 -0
  85. package/public/profile_pictures/library/Cars/151 - gogo_mobile_dart.jpeg +0 -0
  86. package/public/profile_pictures/library/README.md +23 -0
  87. package/public/profile_pictures/library/Young Cartoons/001-Zoe.jpeg +0 -0
  88. package/public/profile_pictures/library/Young Cartoons/002-Nora.jpeg +0 -0
  89. package/public/profile_pictures/library/Young Cartoons/003-Aria.jpeg +0 -0
  90. package/public/profile_pictures/library/Young Cartoons/004-Reagan.jpeg +0 -0
  91. package/public/profile_pictures/library/Young Cartoons/005-Violet.jpeg +0 -0
  92. package/public/profile_pictures/library/Young Cartoons/006-Alexa.jpeg +0 -0
  93. package/public/profile_pictures/library/Young Cartoons/007-Brooklyn.jpeg +0 -0
  94. package/public/profile_pictures/library/Young Cartoons/008-Kinsley.jpeg +0 -0
  95. package/public/profile_pictures/library/Young Cartoons/009-Quinn.jpeg +0 -0
  96. package/public/profile_pictures/library/Young Cartoons/010-Emilia.jpeg +0 -0
  97. package/public/profile_pictures/library/Young Cartoons/011-Mackenzie.jpeg +0 -0
  98. package/public/profile_pictures/library/Young Cartoons/012-Isabella.jpeg +0 -0
  99. package/public/profile_pictures/library/Young Cartoons/013-Sophia.jpeg +0 -0
  100. package/public/profile_pictures/library/Young Cartoons/014-Layla.jpeg +0 -0
  101. package/public/profile_pictures/library/Young Cartoons/015-Sarah.jpeg +0 -0
  102. package/public/profile_pictures/library/Young Cartoons/016-Hadley.jpeg +0 -0
  103. package/public/profile_pictures/library/Young Cartoons/017-Skylar.jpeg +0 -0
  104. package/public/profile_pictures/library/Young Cartoons/018-Brielle.jpeg +0 -0
  105. package/public/profile_pictures/library/Young Cartoons/019-Hailey.jpeg +0 -0
  106. package/public/profile_pictures/library/Young Cartoons/020-Paisley.jpeg +0 -0
  107. package/public/profile_pictures/library/Young Cartoons/021-Nova.jpeg +0 -0
  108. package/public/profile_pictures/library/Young Cartoons/022-Lucy.jpeg +0 -0
  109. package/public/profile_pictures/library/Young Cartoons/023-Ivy.jpeg +0 -0
  110. package/public/profile_pictures/library/Young Cartoons/024-Melanie.jpeg +0 -0
  111. package/public/profile_pictures/library/Young Cartoons/025-Addison.jpeg +0 -0
  112. package/public/profile_pictures/library/Young Cartoons/026-Genesis.jpeg +0 -0
  113. package/public/profile_pictures/library/Young Cartoons/027-Cora.jpeg +0 -0
  114. package/public/profile_pictures/library/Young Cartoons/028-Delilah.jpeg +0 -0
  115. package/public/profile_pictures/library/Young Cartoons/029-Liliana.jpeg +0 -0
  116. package/public/profile_pictures/library/Young Cartoons/030-Savannah.jpeg +0 -0
  117. package/public/profile_pictures/library/Young Cartoons/031-Ava.jpeg +0 -0
  118. package/public/profile_pictures/library/Young Cartoons/032-Aaliyah.jpeg +0 -0
  119. package/public/profile_pictures/library/Young Cartoons/033-Arianna.jpeg +0 -0
  120. package/public/profile_pictures/library/Young Cartoons/034-Abigail.jpeg +0 -0
  121. package/public/profile_pictures/library/Young Cartoons/035-Sophie.jpeg +0 -0
  122. package/public/profile_pictures/library/Young Cartoons/036-Ayla.jpeg +0 -0
  123. package/public/profile_pictures/library/Young Cartoons/037-Gianna.jpeg +0 -0
  124. package/public/profile_pictures/library/Young Cartoons/038-Ella.jpeg +0 -0
  125. package/public/profile_pictures/library/Young Cartoons/039-Mia.jpeg +0 -0
  126. package/public/profile_pictures/library/Young Cartoons/040-Autumn.jpeg +0 -0
  127. package/public/profile_pictures/library/Young Cartoons/041-Luna.jpeg +0 -0
  128. package/public/profile_pictures/library/Young Cartoons/042-Aurora.jpeg +0 -0
  129. package/public/profile_pictures/library/Young Cartoons/043-Samantha.jpeg +0 -0
  130. package/public/profile_pictures/library/Young Cartoons/044-Ariana.jpeg +0 -0
  131. package/public/profile_pictures/library/Young Cartoons/045-Gabriella.jpeg +0 -0
  132. package/public/profile_pictures/library/Young Cartoons/046-Serenity.jpeg +0 -0
  133. package/public/profile_pictures/library/Young Cartoons/047-Caroline.jpeg +0 -0
  134. package/public/profile_pictures/library/Young Cartoons/048-Emma.jpeg +0 -0
  135. package/public/profile_pictures/library/Young Cartoons/049-Piper.jpeg +0 -0
  136. package/public/profile_pictures/library/Young Cartoons/050-Madeline.jpeg +0 -0
  137. package/public/profile_pictures/library/Young Cartoons/051-Elijah.jpeg +0 -0
  138. package/public/profile_pictures/library/Young Cartoons/052-Lucas.jpeg +0 -0
  139. package/public/profile_pictures/library/Young Cartoons/053-Elias.jpeg +0 -0
  140. package/public/profile_pictures/library/Young Cartoons/054-Oliver.jpeg +0 -0
  141. package/public/profile_pictures/library/Young Cartoons/055-Ian.jpeg +0 -0
  142. package/public/profile_pictures/library/Young Cartoons/056-Ryan.jpeg +0 -0
  143. package/public/profile_pictures/library/Young Cartoons/057-Wesley.jpeg +0 -0
  144. package/public/profile_pictures/library/Young Cartoons/058-Joshua.jpeg +0 -0
  145. package/public/profile_pictures/library/Young Cartoons/059-Nicholas.jpeg +0 -0
  146. package/public/profile_pictures/library/Young Cartoons/060-Aiden.jpeg +0 -0
  147. package/public/profile_pictures/library/Young Cartoons/061-Cameron.jpeg +0 -0
  148. package/public/profile_pictures/library/Young Cartoons/062-Logan.jpeg +0 -0
  149. package/public/profile_pictures/library/Young Cartoons/063-Silas.jpeg +0 -0
  150. package/public/profile_pictures/library/Young Cartoons/064-Mateo.jpeg +0 -0
  151. package/public/profile_pictures/library/Young Cartoons/065-Hunter.jpeg +0 -0
  152. package/public/profile_pictures/library/Young Cartoons/066-Colton.jpeg +0 -0
  153. package/public/profile_pictures/library/Young Cartoons/067-Owen.jpeg +0 -0
  154. package/public/profile_pictures/library/Young Cartoons/068-Josiah.jpeg +0 -0
  155. package/public/profile_pictures/library/Young Cartoons/069-Santiago.jpeg +0 -0
  156. package/public/profile_pictures/library/Young Cartoons/070-Lincoln.jpeg +0 -0
  157. package/public/profile_pictures/library/Young Cartoons/071-Samuel.jpeg +0 -0
  158. package/public/profile_pictures/library/Young Cartoons/072-Thomas.jpeg +0 -0
  159. package/public/profile_pictures/library/Young Cartoons/073-Jaxson.jpeg +0 -0
  160. package/public/profile_pictures/library/Young Cartoons/074-Gabriel.jpeg +0 -0
  161. package/public/profile_pictures/library/Young Cartoons/075-Dominic.jpeg +0 -0
  162. package/public/profile_pictures/library/Young Cartoons/076-Grayson.jpeg +0 -0
  163. package/public/profile_pictures/library/Young Cartoons/077-Liam.jpeg +0 -0
  164. package/public/profile_pictures/library/Young Cartoons/078-Ethan.jpeg +0 -0
  165. package/public/profile_pictures/library/Young Cartoons/079-Jaxon.jpeg +0 -0
  166. package/public/profile_pictures/library/Young Cartoons/080-Jackson.jpeg +0 -0
  167. package/public/profile_pictures/library/Young Cartoons/081-Kai.jpeg +0 -0
  168. package/public/profile_pictures/library/Young Cartoons/082-Henry.jpeg +0 -0
  169. package/public/profile_pictures/library/Young Cartoons/083-Hudson.jpeg +0 -0
  170. package/public/profile_pictures/library/Young Cartoons/084-Isaiah.jpeg +0 -0
  171. package/public/profile_pictures/library/Young Cartoons/085-Benjamin.jpeg +0 -0
  172. package/public/profile_pictures/library/Young Cartoons/086-Christopher.jpeg +0 -0
  173. package/public/profile_pictures/library/Young Cartoons/087-Eli.jpeg +0 -0
  174. package/public/profile_pictures/library/Young Cartoons/088-Mason.jpeg +0 -0
  175. package/public/profile_pictures/library/Young Cartoons/089-Jack.jpeg +0 -0
  176. package/public/profile_pictures/library/Young Cartoons/090-Jordan.jpeg +0 -0
  177. package/public/profile_pictures/library/Young Cartoons/091-Easton.jpeg +0 -0
  178. package/public/profile_pictures/library/Young Cartoons/092-Miles.jpeg +0 -0
  179. package/public/profile_pictures/library/Young Cartoons/093-Caleb.jpeg +0 -0
  180. package/public/profile_pictures/library/Young Cartoons/094-Jameson.jpeg +0 -0
  181. package/public/profile_pictures/library/Young Cartoons/095-Kayden.jpeg +0 -0
  182. package/public/profile_pictures/library/Young Cartoons/096-Maverick.jpeg +0 -0
  183. package/public/profile_pictures/library/Young Cartoons/097-Julian.jpeg +0 -0
  184. package/public/profile_pictures/library/Young Cartoons/098-James.jpeg +0 -0
  185. package/public/profile_pictures/library/Young Cartoons/099-Greyson.jpeg +0 -0
  186. package/public/profile_pictures/library/Young Cartoons/100-Robert.jpeg +0 -0
  187. package/public/profile_pictures/library/Young Cartoons/101-Bella.jpeg +0 -0
  188. package/src/app/api/auth/library_photos/route.ts +3 -0
  189. package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +33 -4
  190. package/src/components/layouts/shared/components/profile_pic_menu.tsx +321 -0
  191. package/src/components/layouts/shared/components/profile_pic_menu_wrapper.tsx +40 -0
  192. package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +4 -66
  193. package/src/components/ui/dropdown-menu.tsx +201 -0
  194. package/src/lib/profile_pic_menu_config.server.ts +138 -0
@@ -140,6 +140,21 @@ export function ProfilePictureLibraryTab({
140
140
  return "L";
141
141
  };
142
142
 
143
+ // Map column count to Tailwind grid class
144
+ const getGridColumnsClass = (columns: number): string => {
145
+ const columnMap: Record<number, string> = {
146
+ 1: "grid-cols-1",
147
+ 2: "grid-cols-2",
148
+ 3: "grid-cols-3",
149
+ 4: "grid-cols-4",
150
+ 5: "grid-cols-5",
151
+ 6: "grid-cols-6",
152
+ 7: "grid-cols-7",
153
+ 8: "grid-cols-8",
154
+ };
155
+ return columnMap[columns] || "grid-cols-4";
156
+ };
157
+
143
158
  return (
144
159
  <div className="cls_profile_picture_library_tab flex flex-col gap-4">
145
160
  {/* Switch */}
@@ -213,7 +228,7 @@ export function ProfilePictureLibraryTab({
213
228
  <Loader2 className="h-6 w-6 text-slate-400 animate-spin" aria-hidden="true" />
214
229
  </div>
215
230
  ) : photos.length > 0 ? (
216
- <div className={`cls_profile_picture_library_tab_photos_grid grid grid-cols-${libraryPhotoGridColumns} gap-3 overflow-y-auto p-4 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px] max-h-[400px]`}>
231
+ <div className={`cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-slate-200 rounded-lg bg-slate-50 min-h-[400px] max-h-[400px]`}>
217
232
  {photos.map((photoUrl) => (
218
233
  <button
219
234
  key={photoUrl}
@@ -221,16 +236,21 @@ export function ProfilePictureLibraryTab({
221
236
  onClick={() => handlePhotoClick(photoUrl)}
222
237
  className={`
223
238
  cls_profile_picture_library_tab_photo_thumbnail
224
- aspect-square rounded-lg overflow-hidden border-2 transition-colors
239
+ aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
225
240
  ${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-slate-200 hover:border-slate-300"}
226
241
  `}
227
- aria-label={`Select ${photoUrl}`}
242
+ aria-label={`Select photo ${photoUrl.split('/').pop()}`}
228
243
  >
229
244
  <img
230
245
  src={photoUrl}
231
- alt="Library photo thumbnail"
246
+ alt={`Library photo ${photoUrl.split('/').pop()}`}
232
247
  className="cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover"
233
248
  loading="lazy"
249
+ onError={(e) => {
250
+ // Fallback if image fails to load
251
+ const target = e.target as HTMLImageElement;
252
+ target.style.display = 'none';
253
+ }}
234
254
  />
235
255
  </button>
236
256
  ))}
@@ -256,6 +276,15 @@ export function ProfilePictureLibraryTab({
256
276
  src={selectedPhoto}
257
277
  alt="Selected library photo preview"
258
278
  className="cls_profile_picture_library_tab_preview_image max-w-full max-h-[350px] rounded-lg object-contain"
279
+ onError={(e) => {
280
+ // Fallback if preview image fails to load
281
+ const target = e.target as HTMLImageElement;
282
+ target.style.display = 'none';
283
+ const wrapper = target.parentElement;
284
+ if (wrapper) {
285
+ wrapper.innerHTML = '<p class="text-sm text-red-500">Failed to load preview</p>';
286
+ }
287
+ }}
259
288
  />
260
289
  </div>
261
290
  <p className="cls_profile_picture_library_tab_preview_text text-sm text-slate-600 text-center">
@@ -0,0 +1,321 @@
1
+ // file_description: profile picture menu component for navbar or sidebar - shows profile picture when logged in, or sign up/sign in buttons when not logged in
2
+ // section: client_directive
3
+ "use client";
4
+
5
+ // section: imports
6
+ import { useState, useMemo } from "react";
7
+ import { useRouter } from "next/navigation";
8
+ import Link from "next/link";
9
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
10
+ import { Button } from "@/components/ui/button";
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuItem,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "@/components/ui/dropdown-menu";
18
+ import { Settings, LogOut } from "lucide-react";
19
+ import { toast } from "sonner";
20
+ import { use_auth_status, trigger_auth_status_refresh } from "@/components/layouts/shared/hooks/use_auth_status";
21
+ // Type-only import from server file is safe (types are erased at runtime)
22
+ import type { ProfilePicMenuMenuItem } from "@/lib/profile_pic_menu_config.server";
23
+
24
+ // section: types
25
+ export type ProfilePicMenuProps = {
26
+ show_single_button?: boolean;
27
+ sign_up_label?: string;
28
+ sign_in_label?: string;
29
+ register_path?: string;
30
+ login_path?: string;
31
+ settings_path?: string;
32
+ logout_path?: string;
33
+ custom_menu_items?: ProfilePicMenuMenuItem[];
34
+ className?: string;
35
+ avatar_size?: "default" | "sm" | "lg";
36
+ };
37
+
38
+ // section: component
39
+ /**
40
+ * Profile picture menu component
41
+ * Shows user profile picture when authenticated, or sign up/sign in buttons when not authenticated
42
+ * Clicking profile picture opens dropdown menu with user info and actions
43
+ * @param props - Component props including configuration options
44
+ * @returns Profile picture menu component
45
+ */
46
+ export function ProfilePicMenu({
47
+ show_single_button = false,
48
+ sign_up_label = "Sign Up",
49
+ sign_in_label = "Sign In",
50
+ register_path = "/register",
51
+ login_path = "/login",
52
+ settings_path = "/my_settings",
53
+ logout_path = "/api/auth/logout",
54
+ custom_menu_items = [],
55
+ className,
56
+ avatar_size = "default",
57
+ }: ProfilePicMenuProps) {
58
+ const router = useRouter();
59
+ const authStatus = use_auth_status();
60
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
61
+
62
+ // Get initials from name or email
63
+ const getInitials = (): string => {
64
+ if (authStatus.name) {
65
+ const parts = authStatus.name.trim().split(" ");
66
+ if (parts.length >= 2) {
67
+ return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
68
+ }
69
+ return authStatus.name[0]?.toUpperCase() || "";
70
+ }
71
+ if (authStatus.email) {
72
+ return authStatus.email[0]?.toUpperCase() || "";
73
+ }
74
+ return "?";
75
+ };
76
+
77
+ // Handle logout
78
+ const handleLogout = async () => {
79
+ setIsLoggingOut(true);
80
+
81
+ try {
82
+ const response = await fetch(logout_path, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ },
87
+ });
88
+
89
+ const data = await response.json();
90
+
91
+ if (!response.ok || !data.success) {
92
+ throw new Error(data.error || "Logout failed");
93
+ }
94
+
95
+ toast.success("Logged out successfully");
96
+
97
+ // Trigger auth status refresh in all components
98
+ trigger_auth_status_refresh();
99
+
100
+ // Refresh the page to update authentication state
101
+ router.refresh();
102
+ } catch (error) {
103
+ const errorMessage =
104
+ error instanceof Error ? error.message : "Logout failed. Please try again.";
105
+ toast.error(errorMessage);
106
+ } finally {
107
+ setIsLoggingOut(false);
108
+ }
109
+ };
110
+
111
+ // Build menu items with default items and custom items
112
+ const menuItems = useMemo(() => {
113
+ const items: ProfilePicMenuMenuItem[] = [];
114
+
115
+ // Add default info items (only if authenticated)
116
+ if (authStatus.authenticated) {
117
+ // User name (info, order: 1)
118
+ if (authStatus.name) {
119
+ items.push({
120
+ type: "info",
121
+ value: authStatus.name,
122
+ order: 1,
123
+ id: "default_name",
124
+ });
125
+ }
126
+
127
+ // Email address (info, order: 2)
128
+ if (authStatus.email) {
129
+ items.push({
130
+ type: "info",
131
+ value: authStatus.email,
132
+ order: 2,
133
+ id: "default_email",
134
+ });
135
+ }
136
+
137
+ // Separator (order: 1)
138
+ items.push({
139
+ type: "separator",
140
+ order: 1,
141
+ id: "default_separator",
142
+ });
143
+
144
+ // Settings (link, order: 1)
145
+ items.push({
146
+ type: "link",
147
+ label: "Settings",
148
+ href: settings_path,
149
+ order: 1,
150
+ id: "default_settings",
151
+ });
152
+
153
+ // Logout (link, order: 2)
154
+ items.push({
155
+ type: "link",
156
+ label: "Logout",
157
+ href: logout_path,
158
+ order: 2,
159
+ id: "default_logout",
160
+ });
161
+ }
162
+
163
+ // Add custom menu items
164
+ items.push(...custom_menu_items);
165
+
166
+ // Sort items by type group and order
167
+ // Order: info items first, then separators, then links
168
+ items.sort((a, b) => {
169
+ // Define type priority: info = 0, separator = 1, link = 2
170
+ const typePriority = { info: 0, separator: 1, link: 2 };
171
+ const aPriority = typePriority[a.type];
172
+ const bPriority = typePriority[b.type];
173
+
174
+ if (aPriority !== bPriority) {
175
+ return aPriority - bPriority;
176
+ }
177
+
178
+ // Within same type, sort by order
179
+ return a.order - b.order;
180
+ });
181
+
182
+ return items;
183
+ }, [authStatus.authenticated, authStatus.name, authStatus.email, settings_path, logout_path, custom_menu_items]);
184
+
185
+ // Avatar size classes
186
+ const avatarSizeClasses = {
187
+ sm: "h-8 w-8",
188
+ default: "h-10 w-10",
189
+ lg: "h-12 w-12",
190
+ };
191
+
192
+ // Show loading state
193
+ if (authStatus.loading) {
194
+ return (
195
+ <div className={`cls_profile_pic_menu ${className || ""}`}>
196
+ <div className="h-10 w-10 rounded-full bg-slate-200 animate-pulse" />
197
+ </div>
198
+ );
199
+ }
200
+
201
+ // Not authenticated - show sign up/sign in buttons
202
+ if (!authStatus.authenticated) {
203
+ return (
204
+ <div className={`cls_profile_pic_menu flex items-center gap-2 ${className || ""}`}>
205
+ {show_single_button ? (
206
+ <Button asChild variant="default" size="sm">
207
+ <Link href={register_path} className="cls_profile_pic_menu_sign_up">
208
+ {sign_up_label}
209
+ </Link>
210
+ </Button>
211
+ ) : (
212
+ <>
213
+ <Button asChild variant="outline" size="sm">
214
+ <Link href={register_path} className="cls_profile_pic_menu_sign_up">
215
+ {sign_up_label}
216
+ </Link>
217
+ </Button>
218
+ <Button asChild variant="default" size="sm">
219
+ <Link href={login_path} className="cls_profile_pic_menu_sign_in">
220
+ {sign_in_label}
221
+ </Link>
222
+ </Button>
223
+ </>
224
+ )}
225
+ </div>
226
+ );
227
+ }
228
+
229
+ // Authenticated - show profile picture with dropdown menu
230
+ return (
231
+ <div className={`cls_profile_pic_menu ${className || ""}`}>
232
+ <DropdownMenu>
233
+ <DropdownMenuTrigger asChild>
234
+ <button
235
+ className="cls_profile_pic_menu_trigger focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary rounded-full"
236
+ aria-label="Profile menu"
237
+ >
238
+ <Avatar className={`cls_profile_pic_menu_avatar ${avatarSizeClasses[avatar_size]} cursor-pointer`}>
239
+ <AvatarImage
240
+ src={authStatus.profile_picture_url}
241
+ alt={authStatus.name ? `Profile picture of ${authStatus.name}` : "Profile picture"}
242
+ className="cls_profile_pic_menu_image"
243
+ />
244
+ <AvatarFallback className="cls_profile_pic_menu_fallback bg-slate-200 text-slate-600">
245
+ {getInitials()}
246
+ </AvatarFallback>
247
+ </Avatar>
248
+ </button>
249
+ </DropdownMenuTrigger>
250
+ <DropdownMenuContent align="end" className="cls_profile_pic_menu_dropdown w-56">
251
+ {menuItems.map((item) => {
252
+ if (item.type === "separator") {
253
+ return <DropdownMenuSeparator key={item.id} className="cls_profile_pic_menu_separator" />;
254
+ }
255
+
256
+ if (item.type === "info") {
257
+ return (
258
+ <div key={item.id} className="cls_profile_pic_menu_info">
259
+ {item.value && (
260
+ <div className="cls_profile_pic_menu_info_value px-2 py-1.5 text-sm text-foreground">
261
+ {item.value}
262
+ </div>
263
+ )}
264
+ </div>
265
+ );
266
+ }
267
+
268
+ if (item.type === "link") {
269
+ // Special handling for logout
270
+ if (item.id === "default_logout") {
271
+ return (
272
+ <DropdownMenuItem
273
+ key={item.id}
274
+ onClick={handleLogout}
275
+ disabled={isLoggingOut}
276
+ className="cls_profile_pic_menu_logout cursor-pointer text-destructive focus:text-destructive"
277
+ >
278
+ <LogOut className="mr-2 h-4 w-4" />
279
+ {isLoggingOut ? "Logging out..." : item.label}
280
+ </DropdownMenuItem>
281
+ );
282
+ }
283
+
284
+ // Special handling for settings
285
+ if (item.id === "default_settings") {
286
+ return (
287
+ <DropdownMenuItem
288
+ key={item.id}
289
+ asChild
290
+ className="cls_profile_pic_menu_settings cursor-pointer"
291
+ >
292
+ <Link href={item.href || settings_path}>
293
+ <Settings className="mr-2 h-4 w-4" />
294
+ {item.label}
295
+ </Link>
296
+ </DropdownMenuItem>
297
+ );
298
+ }
299
+
300
+ // Generic link handling
301
+ return (
302
+ <DropdownMenuItem
303
+ key={item.id}
304
+ asChild
305
+ className="cls_profile_pic_menu_link cursor-pointer"
306
+ >
307
+ <Link href={item.href || "#"}>
308
+ {item.label}
309
+ </Link>
310
+ </DropdownMenuItem>
311
+ );
312
+ }
313
+
314
+ return null;
315
+ })}
316
+ </DropdownMenuContent>
317
+ </DropdownMenu>
318
+ </div>
319
+ );
320
+ }
321
+
@@ -0,0 +1,40 @@
1
+ // file_description: server wrapper component that loads profile picture menu configuration and passes to client component
2
+ // section: imports
3
+ import { ProfilePicMenu } from "./profile_pic_menu";
4
+ import { get_profile_pic_menu_config } from "@/lib/profile_pic_menu_config.server";
5
+
6
+ // section: types
7
+ export type ProfilePicMenuWrapperProps = {
8
+ className?: string;
9
+ avatar_size?: "default" | "sm" | "lg";
10
+ };
11
+
12
+ // section: component
13
+ /**
14
+ * Server wrapper component that loads profile picture menu configuration from hazo_auth_config.ini
15
+ * and passes it to the client ProfilePicMenu component
16
+ * @param props - Component props including className and avatar_size
17
+ * @returns ProfilePicMenu component with loaded configuration
18
+ */
19
+ export function ProfilePicMenuWrapper({
20
+ className,
21
+ avatar_size,
22
+ }: ProfilePicMenuWrapperProps) {
23
+ const config = get_profile_pic_menu_config();
24
+
25
+ return (
26
+ <ProfilePicMenu
27
+ show_single_button={config.show_single_button}
28
+ sign_up_label={config.sign_up_label}
29
+ sign_in_label={config.sign_in_label}
30
+ register_path={config.register_path}
31
+ login_path={config.login_path}
32
+ settings_path={config.settings_path}
33
+ logout_path={config.logout_path}
34
+ custom_menu_items={config.custom_menu_items}
35
+ className={className}
36
+ avatar_size={avatar_size}
37
+ />
38
+ );
39
+ }
40
+
@@ -17,11 +17,9 @@ import {
17
17
  SidebarTrigger,
18
18
  SidebarInset,
19
19
  } from "@/components/ui/sidebar";
20
- import { LogIn, UserPlus, BookOpen, ExternalLink, Database, KeyRound, MailCheck, User, LogOut, Key, Settings } from "lucide-react";
21
- import { use_auth_status, trigger_auth_status_refresh } from "@/components/layouts/shared/hooks/use_auth_status";
22
- import { LogoutButton } from "@/components/layouts/shared/components/logout_button";
23
- import { useRouter } from "next/navigation";
24
- import { toast } from "sonner";
20
+ import { LogIn, UserPlus, BookOpen, ExternalLink, Database, KeyRound, MailCheck, Key, Settings } from "lucide-react";
21
+ import { use_auth_status } from "@/components/layouts/shared/hooks/use_auth_status";
22
+ import { ProfilePicMenu } from "@/components/layouts/shared/components/profile_pic_menu";
25
23
 
26
24
  // section: types
27
25
  type SidebarLayoutWrapperProps = {
@@ -31,36 +29,6 @@ type SidebarLayoutWrapperProps = {
31
29
  // section: component
32
30
  export function SidebarLayoutWrapper({ children }: SidebarLayoutWrapperProps) {
33
31
  const authStatus = use_auth_status();
34
- const router = useRouter();
35
-
36
- const handleLogout = async () => {
37
- try {
38
- const response = await fetch("/api/auth/logout", {
39
- method: "POST",
40
- headers: {
41
- "Content-Type": "application/json",
42
- },
43
- });
44
-
45
- const data = await response.json();
46
-
47
- if (!response.ok || !data.success) {
48
- throw new Error(data.error || "Logout failed");
49
- }
50
-
51
- toast.success("Logged out successfully");
52
-
53
- // Trigger auth status refresh in all components (navbar, sidebar, etc.)
54
- trigger_auth_status_refresh();
55
-
56
- // Refresh the page to update authentication state (cookies are cleared server-side)
57
- router.refresh();
58
- } catch (error) {
59
- const errorMessage =
60
- error instanceof Error ? error.message : "Logout failed. Please try again.";
61
- toast.error(errorMessage);
62
- }
63
- };
64
32
 
65
33
  return (
66
34
  <SidebarProvider>
@@ -171,16 +139,6 @@ export function SidebarLayoutWrapper({ children }: SidebarLayoutWrapperProps) {
171
139
  </Link>
172
140
  </SidebarMenuButton>
173
141
  </SidebarMenuItem>
174
- <SidebarMenuItem className="cls_sidebar_layout_logout_item">
175
- <SidebarMenuButton
176
- onClick={handleLogout}
177
- className="cls_sidebar_layout_logout_link flex items-center gap-2"
178
- aria-label="Logout"
179
- >
180
- <LogOut className="h-4 w-4" aria-hidden="true" />
181
- <span>Logout</span>
182
- </SidebarMenuButton>
183
- </SidebarMenuItem>
184
142
  </SidebarMenu>
185
143
  </SidebarGroup>
186
144
  )}
@@ -231,27 +189,7 @@ export function SidebarLayoutWrapper({ children }: SidebarLayoutWrapperProps) {
231
189
  hazo reusable ui library workspace
232
190
  </h2>
233
191
  </div>
234
- <div className="cls_sidebar_layout_auth_status flex items-center gap-3">
235
- {authStatus.loading ? (
236
- <span className="cls_sidebar_layout_auth_loading text-sm text-muted-foreground">
237
- Loading...
238
- </span>
239
- ) : authStatus.authenticated ? (
240
- <>
241
- <div className="cls_sidebar_layout_user_info flex items-center gap-2">
242
- <User className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
243
- <span className="cls_sidebar_layout_user_name text-sm font-medium text-foreground">
244
- {authStatus.name || authStatus.email || "Logged in"}
245
- </span>
246
- </div>
247
- <LogoutButton size="sm" />
248
- </>
249
- ) : (
250
- <span className="cls_sidebar_layout_not_logged_in text-sm text-muted-foreground">
251
- Not logged in
252
- </span>
253
- )}
254
- </div>
192
+ <ProfilePicMenu className="cls_sidebar_layout_auth_status" avatar_size="sm" />
255
193
  </header>
256
194
  <main className="cls_sidebar_layout_main_content flex flex-1 items-center justify-center p-6">
257
195
  {children}