hazo_auth 0.1.2 → 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.
- package/hazo_auth_config.example.ini +36 -0
- package/package.json +2 -1
- package/src/app/api/auth/library_photos/route.ts +3 -0
- package/src/components/layouts/my_settings/components/profile_picture_library_tab.tsx +33 -4
- package/src/components/layouts/shared/components/profile_pic_menu.tsx +321 -0
- package/src/components/layouts/shared/components/profile_pic_menu_wrapper.tsx +40 -0
- package/src/components/layouts/shared/components/sidebar_layout_wrapper.tsx +4 -66
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/lib/profile_pic_menu_config.server.ts +138 -0
|
@@ -251,6 +251,42 @@ enable_admin_ui = true
|
|
|
251
251
|
# Login path (redirect target in unauthorized message)
|
|
252
252
|
# login_path = /login
|
|
253
253
|
|
|
254
|
+
[hazo_auth__profile_pic_menu]
|
|
255
|
+
# Profile picture menu configuration
|
|
256
|
+
# This component can be used in navbar or sidebar to show user profile picture or sign up/sign in buttons
|
|
257
|
+
|
|
258
|
+
# Button configuration for unauthenticated users
|
|
259
|
+
# Show only "Sign Up" button when true, show both "Sign Up" and "Sign In" buttons when false (default)
|
|
260
|
+
# show_single_button = false
|
|
261
|
+
|
|
262
|
+
# Sign up button label
|
|
263
|
+
# sign_up_label = Sign Up
|
|
264
|
+
|
|
265
|
+
# Sign in button label
|
|
266
|
+
# sign_in_label = Sign In
|
|
267
|
+
|
|
268
|
+
# Register page path
|
|
269
|
+
# register_path = /register
|
|
270
|
+
|
|
271
|
+
# Login page path
|
|
272
|
+
# login_path = /login
|
|
273
|
+
|
|
274
|
+
# Settings page path (shown in dropdown menu when authenticated)
|
|
275
|
+
# settings_path = /my_settings
|
|
276
|
+
|
|
277
|
+
# Logout API endpoint path
|
|
278
|
+
# logout_path = /api/auth/logout
|
|
279
|
+
|
|
280
|
+
# Custom menu items (optional)
|
|
281
|
+
# Format: "type:label:value_or_href:order" for info/link, or "separator:order" for separator
|
|
282
|
+
# Examples:
|
|
283
|
+
# - Info item: "info:Phone:+1234567890:3"
|
|
284
|
+
# - Link item: "link:My Account:/account:4"
|
|
285
|
+
# - Separator: "separator:2"
|
|
286
|
+
# Custom items are added to the default menu items (name, email, separator, Settings, Logout)
|
|
287
|
+
# Items are sorted by type (info first, then separators, then links) and then by order within each type
|
|
288
|
+
# custom_menu_items =
|
|
289
|
+
|
|
254
290
|
[hazo_auth__profile_picture]
|
|
255
291
|
# Profile picture configuration
|
|
256
292
|
# This configuration is used by:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"src/**/*",
|
|
6
6
|
"public/file.svg",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
39
39
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
40
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
40
41
|
"@radix-ui/react-label": "^2.1.8",
|
|
41
42
|
"@radix-ui/react-separator": "^1.1.8",
|
|
42
43
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -5,6 +5,9 @@ import { get_library_categories, get_library_photos } from "@/lib/services/profi
|
|
|
5
5
|
import { create_app_logger } from "@/lib/app_logger";
|
|
6
6
|
import { get_filename, get_line_number } from "@/lib/utils/api_route_helpers";
|
|
7
7
|
|
|
8
|
+
// section: route_config
|
|
9
|
+
export const dynamic = 'force-dynamic';
|
|
10
|
+
|
|
8
11
|
// section: api_handler
|
|
9
12
|
export async function GET(request: NextRequest) {
|
|
10
13
|
const logger = create_app_logger();
|
|
@@ -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
|
|
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=
|
|
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,
|
|
21
|
-
import { use_auth_status
|
|
22
|
-
import {
|
|
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
|
-
<
|
|
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}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
5
|
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
12
|
+
|
|
13
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
14
|
+
|
|
15
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
16
|
+
|
|
17
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
18
|
+
|
|
19
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
20
|
+
|
|
21
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
24
|
+
inset?: boolean
|
|
25
|
+
}
|
|
26
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
27
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
31
|
+
inset && "pl-8",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
<ChevronRight className="ml-auto" />
|
|
38
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
39
|
+
))
|
|
40
|
+
DropdownMenuSubTrigger.displayName =
|
|
41
|
+
DropdownMenuPrimitive.SubTrigger.displayName
|
|
42
|
+
|
|
43
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
45
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
46
|
+
>(({ className, ...props }, ref) => (
|
|
47
|
+
<DropdownMenuPrimitive.SubContent
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(
|
|
50
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
DropdownMenuSubContent.displayName =
|
|
57
|
+
DropdownMenuPrimitive.SubContent.displayName
|
|
58
|
+
|
|
59
|
+
const DropdownMenuContent = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
62
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
63
|
+
<DropdownMenuPrimitive.Portal>
|
|
64
|
+
<DropdownMenuPrimitive.Content
|
|
65
|
+
ref={ref}
|
|
66
|
+
sideOffset={sideOffset}
|
|
67
|
+
className={cn(
|
|
68
|
+
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
69
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
|
70
|
+
className
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
</DropdownMenuPrimitive.Portal>
|
|
75
|
+
))
|
|
76
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
77
|
+
|
|
78
|
+
const DropdownMenuItem = React.forwardRef<
|
|
79
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
80
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
81
|
+
inset?: boolean
|
|
82
|
+
}
|
|
83
|
+
>(({ className, inset, ...props }, ref) => (
|
|
84
|
+
<DropdownMenuPrimitive.Item
|
|
85
|
+
ref={ref}
|
|
86
|
+
className={cn(
|
|
87
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
88
|
+
inset && "pl-8",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
))
|
|
94
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
95
|
+
|
|
96
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
97
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
98
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
99
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
100
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn(
|
|
103
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
checked={checked}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
110
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
111
|
+
<Check className="h-4 w-4" />
|
|
112
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
113
|
+
</span>
|
|
114
|
+
{children}
|
|
115
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
116
|
+
))
|
|
117
|
+
DropdownMenuCheckboxItem.displayName =
|
|
118
|
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
119
|
+
|
|
120
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
121
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
122
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
123
|
+
>(({ className, children, ...props }, ref) => (
|
|
124
|
+
<DropdownMenuPrimitive.RadioItem
|
|
125
|
+
ref={ref}
|
|
126
|
+
className={cn(
|
|
127
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
133
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
134
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
135
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
136
|
+
</span>
|
|
137
|
+
{children}
|
|
138
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
139
|
+
))
|
|
140
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
141
|
+
|
|
142
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
143
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
144
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
145
|
+
inset?: boolean
|
|
146
|
+
}
|
|
147
|
+
>(({ className, inset, ...props }, ref) => (
|
|
148
|
+
<DropdownMenuPrimitive.Label
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={cn(
|
|
151
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
152
|
+
inset && "pl-8",
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
))
|
|
158
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
159
|
+
|
|
160
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
161
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
162
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
163
|
+
>(({ className, ...props }, ref) => (
|
|
164
|
+
<DropdownMenuPrimitive.Separator
|
|
165
|
+
ref={ref}
|
|
166
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
))
|
|
170
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
171
|
+
|
|
172
|
+
const DropdownMenuShortcut = ({
|
|
173
|
+
className,
|
|
174
|
+
...props
|
|
175
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
176
|
+
return (
|
|
177
|
+
<span
|
|
178
|
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
DropdownMenu,
|
|
187
|
+
DropdownMenuTrigger,
|
|
188
|
+
DropdownMenuContent,
|
|
189
|
+
DropdownMenuItem,
|
|
190
|
+
DropdownMenuCheckboxItem,
|
|
191
|
+
DropdownMenuRadioItem,
|
|
192
|
+
DropdownMenuLabel,
|
|
193
|
+
DropdownMenuSeparator,
|
|
194
|
+
DropdownMenuShortcut,
|
|
195
|
+
DropdownMenuGroup,
|
|
196
|
+
DropdownMenuPortal,
|
|
197
|
+
DropdownMenuSub,
|
|
198
|
+
DropdownMenuSubContent,
|
|
199
|
+
DropdownMenuSubTrigger,
|
|
200
|
+
DropdownMenuRadioGroup,
|
|
201
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// file_description: server-only helper to read profile picture menu configuration from hazo_auth_config.ini
|
|
2
|
+
// section: imports
|
|
3
|
+
import { get_config_value, get_config_boolean, get_config_array } from "./config/config_loader.server";
|
|
4
|
+
|
|
5
|
+
// section: types
|
|
6
|
+
// Note: These types are also used in client components, but TypeScript types are erased at runtime
|
|
7
|
+
// so importing from a server file is safe for type-only imports
|
|
8
|
+
export type MenuItemType = "info" | "link" | "separator";
|
|
9
|
+
|
|
10
|
+
export type ProfilePicMenuMenuItem = {
|
|
11
|
+
type: MenuItemType;
|
|
12
|
+
label?: string; // For info and link types
|
|
13
|
+
value?: string; // For info type (e.g., user name, email)
|
|
14
|
+
href?: string; // For link type
|
|
15
|
+
order: number; // Ordering within type group
|
|
16
|
+
id: string; // Unique identifier for the item
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ProfilePicMenuConfig = {
|
|
20
|
+
show_single_button: boolean;
|
|
21
|
+
sign_up_label: string;
|
|
22
|
+
sign_in_label: string;
|
|
23
|
+
register_path: string;
|
|
24
|
+
login_path: string;
|
|
25
|
+
settings_path: string;
|
|
26
|
+
logout_path: string;
|
|
27
|
+
custom_menu_items: ProfilePicMenuMenuItem[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// section: helpers
|
|
31
|
+
/**
|
|
32
|
+
* Parses custom menu items from config string
|
|
33
|
+
* Format: "type:label:order" or "type:label:href:order" for links
|
|
34
|
+
* Example: "link:My Custom Link:/custom:3"
|
|
35
|
+
* @param items_string - Comma-separated string of menu items
|
|
36
|
+
* @returns Array of parsed menu items
|
|
37
|
+
*/
|
|
38
|
+
function parse_custom_menu_items(items_string: string[]): ProfilePicMenuMenuItem[] {
|
|
39
|
+
const items: ProfilePicMenuMenuItem[] = [];
|
|
40
|
+
|
|
41
|
+
items_string.forEach((item_string, index) => {
|
|
42
|
+
const parts = item_string.split(":").map((p) => p.trim());
|
|
43
|
+
|
|
44
|
+
if (parts.length < 3) {
|
|
45
|
+
return; // Invalid format, skip
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const type = parts[0] as MenuItemType;
|
|
49
|
+
if (type !== "info" && type !== "link" && type !== "separator") {
|
|
50
|
+
return; // Invalid type, skip
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "separator") {
|
|
54
|
+
const order = parseInt(parts[1] || "1", 10);
|
|
55
|
+
items.push({
|
|
56
|
+
type: "separator",
|
|
57
|
+
order: isNaN(order) ? 1 : order,
|
|
58
|
+
id: `custom_separator_${index}`,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (type === "info") {
|
|
64
|
+
const label = parts[1] || "";
|
|
65
|
+
const value = parts[2] || "";
|
|
66
|
+
const order = parseInt(parts[3] || "1", 10);
|
|
67
|
+
|
|
68
|
+
if (!label || !value) {
|
|
69
|
+
return; // Invalid format, skip
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
items.push({
|
|
73
|
+
type: "info",
|
|
74
|
+
label,
|
|
75
|
+
value,
|
|
76
|
+
order: isNaN(order) ? 1 : order,
|
|
77
|
+
id: `custom_info_${index}`,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (type === "link") {
|
|
83
|
+
const label = parts[1] || "";
|
|
84
|
+
const href = parts[2] || "";
|
|
85
|
+
const order = parseInt(parts[3] || "1", 10);
|
|
86
|
+
|
|
87
|
+
if (!label || !href) {
|
|
88
|
+
return; // Invalid format, skip
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
items.push({
|
|
92
|
+
type: "link",
|
|
93
|
+
label,
|
|
94
|
+
href,
|
|
95
|
+
order: isNaN(order) ? 1 : order,
|
|
96
|
+
id: `custom_link_${index}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return items;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads profile picture menu configuration from hazo_auth_config.ini file
|
|
106
|
+
* Falls back to defaults if hazo_auth_config.ini is not found or section is missing
|
|
107
|
+
* @returns Profile picture menu configuration options
|
|
108
|
+
*/
|
|
109
|
+
export function get_profile_pic_menu_config(): ProfilePicMenuConfig {
|
|
110
|
+
const section = "hazo_auth__profile_pic_menu";
|
|
111
|
+
|
|
112
|
+
// Read button configuration
|
|
113
|
+
const show_single_button = get_config_boolean(section, "show_single_button", false);
|
|
114
|
+
const sign_up_label = get_config_value(section, "sign_up_label", "Sign Up");
|
|
115
|
+
const sign_in_label = get_config_value(section, "sign_in_label", "Sign In");
|
|
116
|
+
const register_path = get_config_value(section, "register_path", "/register");
|
|
117
|
+
const login_path = get_config_value(section, "login_path", "/login");
|
|
118
|
+
|
|
119
|
+
// Read menu paths
|
|
120
|
+
const settings_path = get_config_value(section, "settings_path", "/my_settings");
|
|
121
|
+
const logout_path = get_config_value(section, "logout_path", "/api/auth/logout");
|
|
122
|
+
|
|
123
|
+
// Read custom menu items
|
|
124
|
+
const custom_items_string = get_config_array(section, "custom_menu_items", []);
|
|
125
|
+
const custom_menu_items = parse_custom_menu_items(custom_items_string);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
show_single_button,
|
|
129
|
+
sign_up_label,
|
|
130
|
+
sign_in_label,
|
|
131
|
+
register_path,
|
|
132
|
+
login_path,
|
|
133
|
+
settings_path,
|
|
134
|
+
logout_path,
|
|
135
|
+
custom_menu_items,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|