@chesapeake/stack 0.1.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 (82) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +190 -0
  3. package/README.md +440 -0
  4. package/components/ui/avatar.tsx +87 -0
  5. package/components/ui/badge.tsx +49 -0
  6. package/components/ui/badgeVariants.ts +26 -0
  7. package/components/ui/button-group.tsx +67 -0
  8. package/components/ui/button.tsx +70 -0
  9. package/components/ui/buttonGroupVariants.ts +23 -0
  10. package/components/ui/buttonVariants.ts +38 -0
  11. package/components/ui/calendar.tsx +220 -0
  12. package/components/ui/card.tsx +162 -0
  13. package/components/ui/dialog.tsx +290 -0
  14. package/components/ui/drawer.tsx +227 -0
  15. package/components/ui/dropdown-menu.tsx +260 -0
  16. package/components/ui/index.ts +86 -0
  17. package/components/ui/input.tsx +59 -0
  18. package/components/ui/label.tsx +47 -0
  19. package/components/ui/location-input.md +194 -0
  20. package/components/ui/location-input.tsx +384 -0
  21. package/components/ui/popover.tsx +110 -0
  22. package/components/ui/select.tsx +306 -0
  23. package/components/ui/separator.tsx +50 -0
  24. package/components/ui/sonner-api.ts +8 -0
  25. package/components/ui/sonner.tsx +69 -0
  26. package/components/ui/textarea.tsx +53 -0
  27. package/components/ui/tooltip.tsx +125 -0
  28. package/components/ux/ActivityAnimation.tsx +135 -0
  29. package/components/ux/EmptyState.tsx +22 -0
  30. package/components/ux/FeedbackDrawer.tsx +141 -0
  31. package/components/ux/FireAnimation.tsx +189 -0
  32. package/components/ux/FriendList.tsx +111 -0
  33. package/components/ux/FriendRequestList.tsx +104 -0
  34. package/components/ux/GifSearch.tsx +264 -0
  35. package/components/ux/ImageUpload.tsx +829 -0
  36. package/components/ux/SelectedUsersList.tsx +91 -0
  37. package/components/ux/StandardLayout/PageLayout.tsx +107 -0
  38. package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
  39. package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
  40. package/components/ux/StandardLayout/index.ts +22 -0
  41. package/components/ux/TutorialCarousel.tsx +128 -0
  42. package/components/ux/UserSearch.tsx +324 -0
  43. package/components/ux/UserSelector.tsx +116 -0
  44. package/components/ux/auth/AuthSelector.tsx +154 -0
  45. package/components/ux/auth/MultiAuthProvider.tsx +196 -0
  46. package/components/ux/auth/README.md +317 -0
  47. package/components/ux/auth/context.ts +46 -0
  48. package/components/ux/auth/github/AuthCallback.tsx +177 -0
  49. package/components/ux/auth/github/AuthProvider.tsx +111 -0
  50. package/components/ux/auth/github/README.md +208 -0
  51. package/components/ux/auth/github/client.ts +127 -0
  52. package/components/ux/auth/github/config.ts +68 -0
  53. package/components/ux/auth/github/context.ts +34 -0
  54. package/components/ux/auth/github/default-client.ts +74 -0
  55. package/components/ux/auth/github/index.ts +43 -0
  56. package/components/ux/auth/github/types.ts +44 -0
  57. package/components/ux/auth/github/useGitHubAuth.ts +16 -0
  58. package/components/ux/auth/google/AuthCallback.tsx +170 -0
  59. package/components/ux/auth/google/AuthProvider.tsx +108 -0
  60. package/components/ux/auth/google/README.md +450 -0
  61. package/components/ux/auth/google/client.ts +109 -0
  62. package/components/ux/auth/google/config.ts +65 -0
  63. package/components/ux/auth/google/context.ts +34 -0
  64. package/components/ux/auth/google/default-client.ts +69 -0
  65. package/components/ux/auth/google/index.ts +43 -0
  66. package/components/ux/auth/google/types.ts +42 -0
  67. package/components/ux/auth/google/useGoogleAuth.ts +16 -0
  68. package/components/ux/auth/index.ts +72 -0
  69. package/components/ux/auth/useAuth.ts +16 -0
  70. package/components/ux/index.ts +46 -0
  71. package/components/ux/theme-provider-context.ts +20 -0
  72. package/components/ux/theme-provider.tsx +114 -0
  73. package/components/ux/useTheme.ts +42 -0
  74. package/index.ts +29 -0
  75. package/lib/utils.ts +57 -0
  76. package/package.json +170 -0
  77. package/styles/base.css +38 -0
  78. package/styles/components.css +54 -0
  79. package/styles/index.css +7 -0
  80. package/styles/tokens.css +189 -0
  81. package/tailwind.preset.ts +169 -0
  82. package/types/user.ts +69 -0
@@ -0,0 +1,91 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { Button } from "../ui/button.tsx";
6
+ import { X } from "lucide-react";
7
+
8
+ export interface SelectedUser {
9
+ userId: string;
10
+ displayName: string;
11
+ email?: string;
12
+ profilePicture?: string;
13
+ status?: string;
14
+ }
15
+
16
+ export interface SelectedUsersListProps {
17
+ users: SelectedUser[];
18
+ onRemove: (userId: string) => void;
19
+ showStatus?: boolean;
20
+ maxHeight?: string;
21
+ emptyMessage?: string;
22
+ title?: string;
23
+ }
24
+
25
+ export function SelectedUsersList({
26
+ users,
27
+ onRemove,
28
+ showStatus,
29
+ maxHeight,
30
+ emptyMessage,
31
+ title,
32
+ }: SelectedUsersListProps) {
33
+ if (users.length === 0) {
34
+ return (
35
+ <div className="space-y-2">
36
+ {title && <h3 className="font-medium">{title}</h3>}
37
+ <p className="text-sm text-muted-foreground">{emptyMessage}</p>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <div className="space-y-2">
44
+ {title && <h3 className="font-medium">{title}</h3>}
45
+ <div className="space-y-2 overflow-y-auto" style={{ maxHeight }}>
46
+ {users.map((user) => (
47
+ <div
48
+ key={user.userId}
49
+ className="flex items-center justify-between p-3 border rounded-lg"
50
+ >
51
+ <div className="flex items-center gap-3 flex-1 min-w-0">
52
+ {user.profilePicture && (
53
+ <img
54
+ src={user.profilePicture}
55
+ alt={user.displayName}
56
+ className="w-8 h-8 rounded-full object-cover flex-shrink-0"
57
+ />
58
+ )}
59
+ <div className="flex-1 min-w-0">
60
+ <div className="font-medium truncate">{user.displayName}</div>
61
+ {user.email && (
62
+ <div className="text-sm text-muted-foreground truncate">
63
+ {user.email}
64
+ </div>
65
+ )}
66
+ {showStatus && user.status && (
67
+ <div className="text-xs text-muted-foreground capitalize mt-1">
68
+ {user.status}
69
+ </div>
70
+ )}
71
+ </div>
72
+ </div>
73
+ <div className="ml-4 flex-shrink-0">
74
+ <Button
75
+ type="button"
76
+ variant="destructive"
77
+ size="sm"
78
+ onClick={() => {
79
+ onRemove(user.userId);
80
+ }}
81
+ className="h-8 w-8 p-0"
82
+ >
83
+ <X className="h-4 w-4" />
84
+ </Button>
85
+ </div>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,107 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import * as React from "react";
5
+ import { cn } from "../../../lib/utils";
6
+
7
+ export interface PageLayoutProps {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * PageLayout provides a consistent structure for full-page layouts
14
+ * with proper container, padding, and spacing.
15
+ */
16
+ export function PageLayout({ children, className }: PageLayoutProps) {
17
+ return (
18
+ <div
19
+ className={cn("min-h-screen bg-background text-foreground", className)}
20
+ >
21
+ {children}
22
+ </div>
23
+ );
24
+ }
25
+
26
+ export interface PageHeaderProps {
27
+ children: React.ReactNode;
28
+ className?: string;
29
+ }
30
+
31
+ /**
32
+ * PageHeader provides consistent header styling with border and background.
33
+ * Responsive padding and wrapping for mobile devices.
34
+ */
35
+ export function PageHeader({ children, className }: PageHeaderProps) {
36
+ return (
37
+ <header className={cn("border-b bg-card", className)}>
38
+ <div className="container mx-auto px-4 sm:px-6 py-4">{children}</div>
39
+ </header>
40
+ );
41
+ }
42
+
43
+ export interface PageContentProps {
44
+ children: React.ReactNode;
45
+ className?: string;
46
+ containerClassName?: string;
47
+ }
48
+
49
+ /**
50
+ * PageContent provides consistent main content area with container and padding.
51
+ * Responsive padding for mobile and desktop.
52
+ */
53
+ export function PageContent({
54
+ children,
55
+ className,
56
+ containerClassName,
57
+ }: PageContentProps) {
58
+ return (
59
+ <main
60
+ className={cn(
61
+ "container mx-auto px-4 sm:px-6 py-6 sm:py-8",
62
+ containerClassName
63
+ )}
64
+ >
65
+ <div className={className}>{children}</div>
66
+ </main>
67
+ );
68
+ }
69
+
70
+ export interface PageContainerProps {
71
+ children: React.ReactNode;
72
+ className?: string;
73
+ maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "full";
74
+ }
75
+
76
+ /**
77
+ * PageContainer provides consistent content container with max-width constraints.
78
+ * Useful for centering content with appropriate max-widths.
79
+ * Responsive spacing for mobile and desktop.
80
+ */
81
+ export function PageContainer({
82
+ children,
83
+ className,
84
+ maxWidth,
85
+ }: PageContainerProps) {
86
+ const maxWidthClasses = {
87
+ sm: "max-w-sm",
88
+ md: "max-w-md",
89
+ lg: "max-w-lg",
90
+ xl: "max-w-xl",
91
+ "2xl": "max-w-2xl",
92
+ "4xl": "max-w-4xl",
93
+ full: "max-w-full",
94
+ };
95
+
96
+ return (
97
+ <div
98
+ className={cn(
99
+ "mx-auto space-y-4 sm:space-y-6",
100
+ maxWidth ? maxWidthClasses[maxWidth] : undefined,
101
+ className
102
+ )}
103
+ >
104
+ {children}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,452 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** @jsxImportSource react */
5
+ import { useEffect, useState } from "react";
6
+ import { Button } from "../../../components/ui/button";
7
+ import { Card } from "../../../components/ui/card";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "../../../components/ui/tooltip";
13
+ import { AuthSelector, type AuthUser, useAuth } from "../auth";
14
+ import { FriendList, FriendRequestList, UserSearch } from "../index";
15
+ import type { BaseUser } from "../../../types/user";
16
+ import type { Friend } from "../FriendList";
17
+ import type { FriendRequest } from "../FriendRequestList";
18
+
19
+ export interface ProfilePageProps {
20
+ /** Callback when back button is clicked */
21
+ onBack?: () => void;
22
+ /** Enable Google OAuth login */
23
+ enableGoogle?: boolean;
24
+ /** Enable GitHub OAuth login */
25
+ enableGithub?: boolean;
26
+ /** Callback when authentication is successful */
27
+ onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
28
+ /** Full URL or path for fetching friends list */
29
+ friendsEndpoint?: string;
30
+ /** Full URL or path for fetching friend requests */
31
+ friendRequestsEndpoint?: string;
32
+ /** Full URL or path for user search endpoint */
33
+ userSearchEndpoint?: string;
34
+ /**
35
+ * Base URL for API endpoints.
36
+ * Auth endpoints will be prefixed with this URL.
37
+ * @default ""
38
+ * @example "https://api.example.com" or "/api"
39
+ */
40
+ apiBaseUrl?: string;
41
+ /**
42
+ * Endpoint for checking authentication status.
43
+ * @default "/auth/me"
44
+ */
45
+ authMeEndpoint?: string;
46
+ /**
47
+ * Endpoint for logout.
48
+ * @default "/auth/logout"
49
+ */
50
+ logoutEndpoint?: string;
51
+ /** Additional CSS classes */
52
+ className?: string;
53
+ /** App-specific settings/content to render at the bottom */
54
+ children?: React.ReactNode;
55
+ /** Hide the header (for use in StandardLayout) */
56
+ hideHeader?: boolean;
57
+ }
58
+
59
+ /**
60
+ * ProfilePage component for authentication and user profile display
61
+ * Matches the todo app's ProfilePage styling and structure
62
+ */
63
+ export function ProfilePage({
64
+ onBack,
65
+ enableGoogle,
66
+ enableGithub,
67
+ onAuthSuccess,
68
+ friendsEndpoint,
69
+ friendRequestsEndpoint,
70
+ userSearchEndpoint,
71
+ apiBaseUrl = "",
72
+ authMeEndpoint = "/auth/me",
73
+ logoutEndpoint = "/auth/logout",
74
+ className,
75
+ children,
76
+ hideHeader,
77
+ }: ProfilePageProps) {
78
+ const { signOut: authSignOut } = useAuth();
79
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
80
+ const [userData, setUserData] = useState<{
81
+ userId: string;
82
+ userName: string;
83
+ avatarUrl: string;
84
+ } | null>(null);
85
+ const [friends, setFriends] = useState<Friend[]>([]);
86
+ const [friendRequests, setFriendRequests] = useState<FriendRequest[]>([]);
87
+
88
+ const checkAuthStatus = async () => {
89
+ try {
90
+ const endpoint = `${apiBaseUrl}${authMeEndpoint}`;
91
+ const response = await fetch(endpoint, {
92
+ credentials: "include",
93
+ });
94
+
95
+ if (response.ok) {
96
+ const user = (await response.json()) as {
97
+ userId: string | number;
98
+ userName: string;
99
+ avatarUrl?: string;
100
+ };
101
+ // Validate user data before treating as logged in
102
+ if (user.userId && user.userName) {
103
+ setUserData({
104
+ userId: String(user.userId),
105
+ userName: user.userName,
106
+ avatarUrl: user.avatarUrl ?? "",
107
+ });
108
+ setIsLoggedIn(true);
109
+ } else {
110
+ // Invalid user data, clear auth state
111
+ setUserData(null);
112
+ setIsLoggedIn(false);
113
+ }
114
+ } else {
115
+ // Not authenticated, clear auth state
116
+ setUserData(null);
117
+ setIsLoggedIn(false);
118
+ }
119
+ } catch (err) {
120
+ console.error("Error checking auth status:", err);
121
+ // On error, clear auth state
122
+ setUserData(null);
123
+ setIsLoggedIn(false);
124
+ }
125
+ };
126
+
127
+ const handleLogout = async () => {
128
+ try {
129
+ const endpoint = `${apiBaseUrl}${logoutEndpoint}`;
130
+ await fetch(endpoint, {
131
+ method: "POST",
132
+ credentials: "include",
133
+ });
134
+ authSignOut();
135
+ setUserData(null);
136
+ setIsLoggedIn(false);
137
+ } catch (err) {
138
+ console.error("Logout error:", err);
139
+ // Still clear local state even if server request fails
140
+ authSignOut();
141
+ setUserData(null);
142
+ setIsLoggedIn(false);
143
+ }
144
+ };
145
+
146
+ interface FriendResponse {
147
+ friendId: string;
148
+ friend?: {
149
+ displayName?: string;
150
+ email?: string;
151
+ profilePicture?: string;
152
+ };
153
+ status?: string;
154
+ presence?: string | number;
155
+ }
156
+
157
+ const loadFriends = async () => {
158
+ try {
159
+ const response = await fetch(friendsEndpoint ?? "", {
160
+ credentials: "include",
161
+ });
162
+ if (response.ok) {
163
+ const data = (await response.json()) as {
164
+ value?: FriendResponse[];
165
+ };
166
+ const friendsList: Friend[] = (data.value ?? []).map(
167
+ (f: FriendResponse) => ({
168
+ id: f.friendId,
169
+ displayName: f.friend?.displayName ?? "Unknown",
170
+ email: f.friend?.email,
171
+ profilePicture: f.friend?.profilePicture,
172
+ status:
173
+ f.status === "pending" || f.status === "accepted"
174
+ ? f.status
175
+ : "accepted",
176
+ presence: (() => {
177
+ if (f.presence === undefined) return undefined;
178
+ if (typeof f.presence === "number") return f.presence;
179
+ const parsed = Number.parseFloat(f.presence);
180
+ return Number.isNaN(parsed) ? undefined : parsed;
181
+ })(),
182
+ })
183
+ );
184
+ setFriends(friendsList);
185
+ }
186
+ } catch (error) {
187
+ console.error("Error loading friends:", error);
188
+ }
189
+ };
190
+
191
+ const loadFriendRequests = async () => {
192
+ try {
193
+ const response = await fetch(friendRequestsEndpoint ?? "", {
194
+ credentials: "include",
195
+ });
196
+ if (response.ok) {
197
+ const data = (await response.json()) as {
198
+ value?: FriendRequest[];
199
+ };
200
+ setFriendRequests(data.value ?? []);
201
+ }
202
+ } catch (error) {
203
+ console.error("Error loading friend requests:", error);
204
+ }
205
+ };
206
+
207
+ // Load friends and friend requests when logged in
208
+ useEffect(() => {
209
+ if (isLoggedIn && userData?.userId) {
210
+ void loadFriends();
211
+ void loadFriendRequests();
212
+ }
213
+ /* eslint-disable react-hooks/exhaustive-deps */
214
+ }, [isLoggedIn, userData]);
215
+ /* eslint-enable react-hooks/exhaustive-deps */
216
+
217
+ const handleUserSelect = async (user: BaseUser) => {
218
+ try {
219
+ const response = await fetch(`${friendsEndpoint ?? ""}/${user.id}`, {
220
+ method: "POST",
221
+ credentials: "include",
222
+ });
223
+ if (response.ok) {
224
+ // Reload friend requests to show the sent request
225
+ void loadFriendRequests();
226
+ } else {
227
+ const errorData = (await response.json().catch(() => ({}))) as {
228
+ error?: string;
229
+ };
230
+ alert(errorData.error ?? "Failed to send friend request");
231
+ }
232
+ } catch (error) {
233
+ console.error("Error sending friend request:", error);
234
+ alert("Failed to send friend request");
235
+ }
236
+ };
237
+
238
+ const handleRemoveFriend = async (friendId: string) => {
239
+ try {
240
+ const response = await fetch(`${friendsEndpoint ?? ""}/${friendId}`, {
241
+ method: "DELETE",
242
+ credentials: "include",
243
+ });
244
+ if (response.ok) {
245
+ void loadFriends();
246
+ }
247
+ } catch (error) {
248
+ console.error("Error removing friend:", error);
249
+ }
250
+ };
251
+
252
+ const handleAcceptRequest = async (requestId: string) => {
253
+ try {
254
+ const response = await fetch(
255
+ `${friendRequestsEndpoint ?? ""}/${requestId}/accept`,
256
+ {
257
+ method: "PUT",
258
+ credentials: "include",
259
+ }
260
+ );
261
+ if (response.ok) {
262
+ void loadFriends();
263
+ void loadFriendRequests();
264
+ }
265
+ } catch (error) {
266
+ console.error("Error accepting friend request:", error);
267
+ }
268
+ };
269
+
270
+ const handleRejectRequest = async (requestId: string) => {
271
+ try {
272
+ const response = await fetch(
273
+ `${friendRequestsEndpoint ?? ""}/${requestId}/reject`,
274
+ {
275
+ method: "PUT",
276
+ credentials: "include",
277
+ }
278
+ );
279
+ if (response.ok) {
280
+ void loadFriendRequests();
281
+ }
282
+ } catch (error) {
283
+ console.error("Error rejecting friend request:", error);
284
+ }
285
+ };
286
+
287
+ const handleAuthSuccess = (user: AuthUser, provider: "google" | "github") => {
288
+ // Sync with backend after successful auth
289
+ void checkAuthStatus();
290
+ if (onAuthSuccess) {
291
+ onAuthSuccess(user, provider);
292
+ }
293
+ };
294
+
295
+ return (
296
+ <div className={`max-w-md mx-auto p-4 ${className ?? ""}`}>
297
+ {/* Header */}
298
+ {!hideHeader && (
299
+ <div className="flex items-center justify-between mb-6">
300
+ {onBack && (
301
+ <Button variant="ghost" onClick={onBack} className="p-2">
302
+ <svg
303
+ xmlns="http://www.w3.org/2000/svg"
304
+ width="20"
305
+ height="20"
306
+ viewBox="0 0 24 24"
307
+ fill="none"
308
+ stroke="currentColor"
309
+ strokeWidth="2"
310
+ strokeLinecap="round"
311
+ strokeLinejoin="round"
312
+ >
313
+ <path d="m15 18-6-6 6-6" />
314
+ </svg>
315
+ </Button>
316
+ )}
317
+ <h1 className="text-2xl font-bold">Profile</h1>
318
+ <div className="w-10" /> {/* Spacer for centering */}
319
+ </div>
320
+ )}
321
+
322
+ {/* Authentication Section */}
323
+ {isLoggedIn && userData?.userId && userData.userName ? (
324
+ <Card className="p-6 flex flex-col items-center justify-center">
325
+ <div className="text-center w-full">
326
+ <Tooltip>
327
+ <TooltipTrigger asChild>
328
+ <img
329
+ src={userData.avatarUrl}
330
+ alt={userData.userName}
331
+ className="w-20 h-20 rounded-full mx-auto mb-4 cursor-pointer"
332
+ />
333
+ </TooltipTrigger>
334
+ <TooltipContent>
335
+ <p>{userData.userName}</p>
336
+ {userData.userId && (
337
+ <p className="text-xs text-muted-foreground">
338
+ User ID: {userData.userId}
339
+ </p>
340
+ )}
341
+ </TooltipContent>
342
+ </Tooltip>
343
+ <h2 className="text-xl font-semibold mb-2">@{userData.userName}</h2>
344
+ <Button
345
+ onClick={() => {
346
+ void handleLogout();
347
+ }}
348
+ variant="outline"
349
+ >
350
+ Sign Out
351
+ </Button>
352
+ </div>
353
+ </Card>
354
+ ) : (
355
+ <>
356
+ {/* GitHub Login Section */}
357
+ {enableGithub && (
358
+ <Card className="p-6">
359
+ <div className="text-center w-full">
360
+ <h2 className="text-xl font-semibold mb-2">GitHub Login</h2>
361
+ <p className="text-muted-foreground font-medium mb-4">
362
+ Connect your GitHub account to sync your data and collaborate.
363
+ </p>
364
+ <AuthSelector
365
+ enableGithub
366
+ enableGoogle={false}
367
+ onAuthSuccess={(
368
+ user: AuthUser,
369
+ provider: "google" | "github"
370
+ ) => {
371
+ handleAuthSuccess(user, provider);
372
+ }}
373
+ />
374
+ </div>
375
+ </Card>
376
+ )}
377
+
378
+ {/* Google Login Section */}
379
+ {enableGoogle && (
380
+ <Card className={`p-6 ${(enableGithub ?? "") ? "mt-4" : ""}`}>
381
+ <div className="text-center w-full">
382
+ <h2 className="text-xl font-semibold mb-2">Google Login</h2>
383
+ <p className="text-muted-foreground font-medium mb-4">
384
+ Connect your Google account to sync your data and collaborate.
385
+ </p>
386
+ <AuthSelector
387
+ enableGithub={false}
388
+ enableGoogle
389
+ onAuthSuccess={(
390
+ user: AuthUser,
391
+ provider: "google" | "github"
392
+ ) => {
393
+ handleAuthSuccess(user, provider);
394
+ }}
395
+ />
396
+ </div>
397
+ </Card>
398
+ )}
399
+ </>
400
+ )}
401
+
402
+ {/* Friends Management Section - Only for logged-in users */}
403
+ {isLoggedIn && userData?.userId && (
404
+ <Card className="p-6 mt-4">
405
+ <h3 className="text-lg font-semibold mb-4">Friends</h3>
406
+
407
+ {/* Friend Requests */}
408
+ <div className="mb-6">
409
+ <FriendRequestList
410
+ requests={friendRequests}
411
+ onAccept={(id) => {
412
+ void handleAcceptRequest(id);
413
+ }}
414
+ onReject={(id) => {
415
+ void handleRejectRequest(id);
416
+ }}
417
+ title="Friend Requests"
418
+ />
419
+ </div>
420
+
421
+ {/* Add Friend Search */}
422
+ <div className="mb-6">
423
+ <UserSearch
424
+ onUserSelect={(user) => {
425
+ void handleUserSelect(user);
426
+ }}
427
+ currentUserId={userData.userId}
428
+ searchEndpoint={userSearchEndpoint}
429
+ placeholder="Search for friends..."
430
+ label="Add Friend"
431
+ />
432
+ </div>
433
+
434
+ {/* Friends List */}
435
+ <div>
436
+ <FriendList
437
+ friends={friends}
438
+ onRemove={(id) => {
439
+ void handleRemoveFriend(id);
440
+ }}
441
+ showPresence
442
+ title="My Friends"
443
+ />
444
+ </div>
445
+ </Card>
446
+ )}
447
+
448
+ {/* App-Specific Settings Section */}
449
+ {children && <div className="mt-4">{children}</div>}
450
+ </div>
451
+ );
452
+ }