@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +190 -0
- package/README.md +440 -0
- package/components/ui/avatar.tsx +87 -0
- package/components/ui/badge.tsx +49 -0
- package/components/ui/badgeVariants.ts +26 -0
- package/components/ui/button-group.tsx +67 -0
- package/components/ui/button.tsx +70 -0
- package/components/ui/buttonGroupVariants.ts +23 -0
- package/components/ui/buttonVariants.ts +38 -0
- package/components/ui/calendar.tsx +220 -0
- package/components/ui/card.tsx +162 -0
- package/components/ui/dialog.tsx +290 -0
- package/components/ui/drawer.tsx +227 -0
- package/components/ui/dropdown-menu.tsx +260 -0
- package/components/ui/index.ts +86 -0
- package/components/ui/input.tsx +59 -0
- package/components/ui/label.tsx +47 -0
- package/components/ui/location-input.md +194 -0
- package/components/ui/location-input.tsx +384 -0
- package/components/ui/popover.tsx +110 -0
- package/components/ui/select.tsx +306 -0
- package/components/ui/separator.tsx +50 -0
- package/components/ui/sonner-api.ts +8 -0
- package/components/ui/sonner.tsx +69 -0
- package/components/ui/textarea.tsx +53 -0
- package/components/ui/tooltip.tsx +125 -0
- package/components/ux/ActivityAnimation.tsx +135 -0
- package/components/ux/EmptyState.tsx +22 -0
- package/components/ux/FeedbackDrawer.tsx +141 -0
- package/components/ux/FireAnimation.tsx +189 -0
- package/components/ux/FriendList.tsx +111 -0
- package/components/ux/FriendRequestList.tsx +104 -0
- package/components/ux/GifSearch.tsx +264 -0
- package/components/ux/ImageUpload.tsx +829 -0
- package/components/ux/SelectedUsersList.tsx +91 -0
- package/components/ux/StandardLayout/PageLayout.tsx +107 -0
- package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
- package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
- package/components/ux/StandardLayout/index.ts +22 -0
- package/components/ux/TutorialCarousel.tsx +128 -0
- package/components/ux/UserSearch.tsx +324 -0
- package/components/ux/UserSelector.tsx +116 -0
- package/components/ux/auth/AuthSelector.tsx +154 -0
- package/components/ux/auth/MultiAuthProvider.tsx +196 -0
- package/components/ux/auth/README.md +317 -0
- package/components/ux/auth/context.ts +46 -0
- package/components/ux/auth/github/AuthCallback.tsx +177 -0
- package/components/ux/auth/github/AuthProvider.tsx +111 -0
- package/components/ux/auth/github/README.md +208 -0
- package/components/ux/auth/github/client.ts +127 -0
- package/components/ux/auth/github/config.ts +68 -0
- package/components/ux/auth/github/context.ts +34 -0
- package/components/ux/auth/github/default-client.ts +74 -0
- package/components/ux/auth/github/index.ts +43 -0
- package/components/ux/auth/github/types.ts +44 -0
- package/components/ux/auth/github/useGitHubAuth.ts +16 -0
- package/components/ux/auth/google/AuthCallback.tsx +170 -0
- package/components/ux/auth/google/AuthProvider.tsx +108 -0
- package/components/ux/auth/google/README.md +450 -0
- package/components/ux/auth/google/client.ts +109 -0
- package/components/ux/auth/google/config.ts +65 -0
- package/components/ux/auth/google/context.ts +34 -0
- package/components/ux/auth/google/default-client.ts +69 -0
- package/components/ux/auth/google/index.ts +43 -0
- package/components/ux/auth/google/types.ts +42 -0
- package/components/ux/auth/google/useGoogleAuth.ts +16 -0
- package/components/ux/auth/index.ts +72 -0
- package/components/ux/auth/useAuth.ts +16 -0
- package/components/ux/index.ts +46 -0
- package/components/ux/theme-provider-context.ts +20 -0
- package/components/ux/theme-provider.tsx +114 -0
- package/components/ux/useTheme.ts +42 -0
- package/index.ts +29 -0
- package/lib/utils.ts +57 -0
- package/package.json +170 -0
- package/styles/base.css +38 -0
- package/styles/components.css +54 -0
- package/styles/index.css +7 -0
- package/styles/tokens.css +189 -0
- package/tailwind.preset.ts +169 -0
- package/types/user.ts +69 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** @jsxImportSource react */
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Button } from "../../../components/ui/button";
|
|
7
|
+
import { useAuth } from "../auth";
|
|
8
|
+
import { ProfilePage } from "./ProfilePage";
|
|
9
|
+
import type { AuthUser } from "../auth/context";
|
|
10
|
+
|
|
11
|
+
export type MaxWidthPreset = "narrow" | "medium" | "wide";
|
|
12
|
+
|
|
13
|
+
export interface StandardLayoutProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
headerContent?: React.ReactNode; // Custom header content (title, buttons, etc.)
|
|
16
|
+
showProfileButton?: boolean; // Default: true
|
|
17
|
+
profilePageChildren?: React.ReactNode; // App-specific settings for ProfilePage
|
|
18
|
+
// ProfilePage props
|
|
19
|
+
enableGoogle?: boolean;
|
|
20
|
+
enableGithub?: boolean;
|
|
21
|
+
onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
|
|
22
|
+
friendsEndpoint?: string;
|
|
23
|
+
friendRequestsEndpoint?: string;
|
|
24
|
+
userSearchEndpoint?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
maxWidth?: MaxWidthPreset; // Default: "wide" (100%)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* StandardLayout provides a complete single-page application layout
|
|
31
|
+
* with header, main content area, and integrated profile page.
|
|
32
|
+
* Matches the todo app's layout structure.
|
|
33
|
+
*/
|
|
34
|
+
export function StandardLayout({
|
|
35
|
+
children,
|
|
36
|
+
headerContent,
|
|
37
|
+
showProfileButton,
|
|
38
|
+
profilePageChildren,
|
|
39
|
+
enableGoogle,
|
|
40
|
+
enableGithub,
|
|
41
|
+
onAuthSuccess,
|
|
42
|
+
friendsEndpoint,
|
|
43
|
+
friendRequestsEndpoint,
|
|
44
|
+
userSearchEndpoint,
|
|
45
|
+
className,
|
|
46
|
+
maxWidth,
|
|
47
|
+
}: StandardLayoutProps) {
|
|
48
|
+
const [showProfile, setShowProfile] = useState(false);
|
|
49
|
+
const { user: authUser } = useAuth();
|
|
50
|
+
|
|
51
|
+
// Determine if user is logged in based on authUser
|
|
52
|
+
const isLoggedIn = !!(
|
|
53
|
+
authUser &&
|
|
54
|
+
typeof authUser === "object" &&
|
|
55
|
+
Object.keys(authUser).length > 0
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Get user avatar from authUser if available
|
|
59
|
+
const userAvatar =
|
|
60
|
+
authUser && typeof authUser === "object" && "avatarUrl" in authUser
|
|
61
|
+
? (authUser.avatarUrl as string)
|
|
62
|
+
: authUser && typeof authUser === "object" && "photoURL" in authUser
|
|
63
|
+
? (authUser.photoURL as string)
|
|
64
|
+
: undefined;
|
|
65
|
+
|
|
66
|
+
const handleProfileClick = () => {
|
|
67
|
+
setShowProfile(true);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleBackToMain = () => {
|
|
71
|
+
setShowProfile(false);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Get responsive max-width class
|
|
75
|
+
// 100% width at 1080p and below (1920px), 50% width above 1080p
|
|
76
|
+
const getMaxWidthClass = (preset: MaxWidthPreset | undefined): string => {
|
|
77
|
+
const p = preset ?? "wide";
|
|
78
|
+
switch (p) {
|
|
79
|
+
case "narrow":
|
|
80
|
+
// Responsive: 100% below 1920px (1080p), 50% at 1920px and above
|
|
81
|
+
return "max-w-full [1920px]:max-w-[50%]";
|
|
82
|
+
case "medium":
|
|
83
|
+
// Responsive: 100% below 1920px, 75% at 1920px and above
|
|
84
|
+
return "max-w-full [1920px]:max-w-[75%]";
|
|
85
|
+
case "wide":
|
|
86
|
+
return "max-w-full";
|
|
87
|
+
default:
|
|
88
|
+
return "max-w-full";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Show profile page if profile is open
|
|
93
|
+
if (showProfile) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
|
|
97
|
+
>
|
|
98
|
+
{/* Toolbar/Header */}
|
|
99
|
+
<div className="border-b bg-background flex-shrink-0">
|
|
100
|
+
<div className="flex justify-center">
|
|
101
|
+
<div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
|
|
102
|
+
<div className="flex justify-between items-center">
|
|
103
|
+
{headerContent ? (
|
|
104
|
+
<div className="flex items-center gap-2 w-full">
|
|
105
|
+
{headerContent}
|
|
106
|
+
{showProfileButton && (
|
|
107
|
+
<div className="ml-auto">
|
|
108
|
+
<Button
|
|
109
|
+
variant="outline"
|
|
110
|
+
size="icon"
|
|
111
|
+
className={isLoggedIn ? "ring-2 ring-green-500" : ""}
|
|
112
|
+
onClick={handleProfileClick}
|
|
113
|
+
title={isLoggedIn ? "View Profile" : "Login"}
|
|
114
|
+
>
|
|
115
|
+
{isLoggedIn && userAvatar ? (
|
|
116
|
+
<img
|
|
117
|
+
src={userAvatar}
|
|
118
|
+
alt="Profile"
|
|
119
|
+
className="w-6 h-6 rounded-full"
|
|
120
|
+
/>
|
|
121
|
+
) : (
|
|
122
|
+
<svg
|
|
123
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
124
|
+
width="24"
|
|
125
|
+
height="24"
|
|
126
|
+
viewBox="0 0 24 24"
|
|
127
|
+
fill="none"
|
|
128
|
+
stroke="currentColor"
|
|
129
|
+
strokeWidth="2"
|
|
130
|
+
strokeLinecap="round"
|
|
131
|
+
strokeLinejoin="round"
|
|
132
|
+
>
|
|
133
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
134
|
+
<circle cx="12" cy="7" r="4" />
|
|
135
|
+
</svg>
|
|
136
|
+
)}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<>
|
|
143
|
+
<div className="flex items-center gap-2">
|
|
144
|
+
<h1 className="text-2xl font-bold">App</h1>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
{showProfileButton && (
|
|
148
|
+
<Button
|
|
149
|
+
variant="outline"
|
|
150
|
+
size="icon"
|
|
151
|
+
className={isLoggedIn ? "ring-2 ring-green-500" : ""}
|
|
152
|
+
onClick={handleProfileClick}
|
|
153
|
+
title={isLoggedIn ? "View Profile" : "Login"}
|
|
154
|
+
>
|
|
155
|
+
{isLoggedIn && userAvatar ? (
|
|
156
|
+
<img
|
|
157
|
+
src={userAvatar}
|
|
158
|
+
alt="Profile"
|
|
159
|
+
className="w-6 h-6 rounded-full"
|
|
160
|
+
/>
|
|
161
|
+
) : (
|
|
162
|
+
<svg
|
|
163
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
164
|
+
width="24"
|
|
165
|
+
height="24"
|
|
166
|
+
viewBox="0 0 24 24"
|
|
167
|
+
fill="none"
|
|
168
|
+
stroke="currentColor"
|
|
169
|
+
strokeWidth="2"
|
|
170
|
+
strokeLinecap="round"
|
|
171
|
+
strokeLinejoin="round"
|
|
172
|
+
>
|
|
173
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
174
|
+
<circle cx="12" cy="7" r="4" />
|
|
175
|
+
</svg>
|
|
176
|
+
)}
|
|
177
|
+
</Button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Main content area - Profile Page */}
|
|
188
|
+
<div className="flex-1 overflow-hidden flex justify-center min-h-0">
|
|
189
|
+
<div
|
|
190
|
+
className={`w-full ${getMaxWidthClass(maxWidth)} flex flex-col h-full min-h-0`}
|
|
191
|
+
>
|
|
192
|
+
{/* Fixed Profile Header */}
|
|
193
|
+
<div className="flex-shrink-0 border-b bg-background p-4">
|
|
194
|
+
<div className="flex items-center justify-between">
|
|
195
|
+
<Button
|
|
196
|
+
variant="ghost"
|
|
197
|
+
onClick={handleBackToMain}
|
|
198
|
+
className="p-2"
|
|
199
|
+
>
|
|
200
|
+
<svg
|
|
201
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
202
|
+
width="20"
|
|
203
|
+
height="20"
|
|
204
|
+
viewBox="0 0 24 24"
|
|
205
|
+
fill="none"
|
|
206
|
+
stroke="currentColor"
|
|
207
|
+
strokeWidth="2"
|
|
208
|
+
strokeLinecap="round"
|
|
209
|
+
strokeLinejoin="round"
|
|
210
|
+
>
|
|
211
|
+
<path d="m15 18-6-6 6-6" />
|
|
212
|
+
</svg>
|
|
213
|
+
</Button>
|
|
214
|
+
<h1 className="text-2xl font-bold">Profile</h1>
|
|
215
|
+
<div className="w-10" /> {/* Spacer for centering */}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Scrollable Profile Content */}
|
|
220
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
221
|
+
<ProfilePage
|
|
222
|
+
onBack={undefined}
|
|
223
|
+
enableGoogle={enableGoogle}
|
|
224
|
+
enableGithub={enableGithub}
|
|
225
|
+
onAuthSuccess={onAuthSuccess}
|
|
226
|
+
friendsEndpoint={friendsEndpoint}
|
|
227
|
+
friendRequestsEndpoint={friendRequestsEndpoint}
|
|
228
|
+
userSearchEndpoint={userSearchEndpoint}
|
|
229
|
+
className={className}
|
|
230
|
+
hideHeader
|
|
231
|
+
>
|
|
232
|
+
{profilePageChildren}
|
|
233
|
+
</ProfilePage>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Render main layout
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
className={`h-screen flex flex-col overflow-hidden ${className ?? ""}`}
|
|
245
|
+
>
|
|
246
|
+
{/* Toolbar/Header */}
|
|
247
|
+
<div className="border-b bg-background flex-shrink-0">
|
|
248
|
+
<div className="flex justify-center">
|
|
249
|
+
<div className={`w-full ${getMaxWidthClass(maxWidth)} p-4`}>
|
|
250
|
+
<div className="flex justify-between items-center">
|
|
251
|
+
{headerContent ? (
|
|
252
|
+
<div className="flex items-center gap-2 w-full">
|
|
253
|
+
{headerContent}
|
|
254
|
+
{showProfileButton && (
|
|
255
|
+
<div className="ml-auto">
|
|
256
|
+
<Button
|
|
257
|
+
variant="outline"
|
|
258
|
+
size="icon"
|
|
259
|
+
className={isLoggedIn ? "ring-2 ring-green-500" : ""}
|
|
260
|
+
onClick={handleProfileClick}
|
|
261
|
+
title={isLoggedIn ? "View Profile" : "Login"}
|
|
262
|
+
>
|
|
263
|
+
{isLoggedIn && userAvatar ? (
|
|
264
|
+
<img
|
|
265
|
+
src={userAvatar}
|
|
266
|
+
alt="Profile"
|
|
267
|
+
className="w-6 h-6 rounded-full"
|
|
268
|
+
/>
|
|
269
|
+
) : (
|
|
270
|
+
<svg
|
|
271
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
272
|
+
width="24"
|
|
273
|
+
height="24"
|
|
274
|
+
viewBox="0 0 24 24"
|
|
275
|
+
fill="none"
|
|
276
|
+
stroke="currentColor"
|
|
277
|
+
strokeWidth="2"
|
|
278
|
+
strokeLinecap="round"
|
|
279
|
+
strokeLinejoin="round"
|
|
280
|
+
>
|
|
281
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
282
|
+
<circle cx="12" cy="7" r="4" />
|
|
283
|
+
</svg>
|
|
284
|
+
)}
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
<>
|
|
291
|
+
<div className="flex items-center gap-2">
|
|
292
|
+
<h1 className="text-2xl font-bold">App</h1>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="flex items-center gap-2">
|
|
295
|
+
{showProfileButton && (
|
|
296
|
+
<Button
|
|
297
|
+
variant="outline"
|
|
298
|
+
size="icon"
|
|
299
|
+
className={isLoggedIn ? "ring-2 ring-green-500" : ""}
|
|
300
|
+
onClick={handleProfileClick}
|
|
301
|
+
title={isLoggedIn ? "View Profile" : "Login"}
|
|
302
|
+
>
|
|
303
|
+
{isLoggedIn && userAvatar ? (
|
|
304
|
+
<img
|
|
305
|
+
src={userAvatar}
|
|
306
|
+
alt="Profile"
|
|
307
|
+
className="w-6 h-6 rounded-full"
|
|
308
|
+
/>
|
|
309
|
+
) : (
|
|
310
|
+
<svg
|
|
311
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
312
|
+
width="24"
|
|
313
|
+
height="24"
|
|
314
|
+
viewBox="0 0 24 24"
|
|
315
|
+
fill="none"
|
|
316
|
+
stroke="currentColor"
|
|
317
|
+
strokeWidth="2"
|
|
318
|
+
strokeLinecap="round"
|
|
319
|
+
strokeLinejoin="round"
|
|
320
|
+
>
|
|
321
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
322
|
+
<circle cx="12" cy="7" r="4" />
|
|
323
|
+
</svg>
|
|
324
|
+
)}
|
|
325
|
+
</Button>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{/* Main content area */}
|
|
336
|
+
<div className="flex-1 overflow-hidden flex justify-center min-h-0">
|
|
337
|
+
<div
|
|
338
|
+
className={`w-full ${getMaxWidthClass(maxWidth)} flex flex-col h-full min-h-0`}
|
|
339
|
+
>
|
|
340
|
+
{children}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// StandardLayout exports
|
|
5
|
+
export { StandardLayout } from "./StandardLayout.tsx";
|
|
6
|
+
export type { StandardLayoutProps } from "./StandardLayout.tsx";
|
|
7
|
+
|
|
8
|
+
export { ProfilePage } from "./ProfilePage.tsx";
|
|
9
|
+
export type { ProfilePageProps } from "./ProfilePage.tsx";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
PageContainer,
|
|
13
|
+
PageContent,
|
|
14
|
+
PageHeader,
|
|
15
|
+
PageLayout,
|
|
16
|
+
} from "./PageLayout.tsx";
|
|
17
|
+
export type {
|
|
18
|
+
PageContainerProps,
|
|
19
|
+
PageContentProps,
|
|
20
|
+
PageHeaderProps,
|
|
21
|
+
PageLayoutProps,
|
|
22
|
+
} from "./PageLayout.tsx";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** @jsxImportSource react */
|
|
5
|
+
import React, { useState } from "react";
|
|
6
|
+
|
|
7
|
+
interface TutorialSlide {
|
|
8
|
+
id: number;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TutorialCarouselProps {
|
|
16
|
+
onComplete: () => void;
|
|
17
|
+
Button: React.ComponentType<{
|
|
18
|
+
variant?: string;
|
|
19
|
+
size?: string;
|
|
20
|
+
onClick?: () => void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
}>;
|
|
25
|
+
slides: TutorialSlide[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const colorMap: Record<string, string> = {
|
|
29
|
+
blue: "text-blue-500",
|
|
30
|
+
green: "text-green-500",
|
|
31
|
+
purple: "text-purple-500",
|
|
32
|
+
red: "text-red-500",
|
|
33
|
+
orange: "text-orange-500",
|
|
34
|
+
yellow: "text-yellow-500",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function TutorialCarousel({
|
|
38
|
+
onComplete,
|
|
39
|
+
Button,
|
|
40
|
+
slides,
|
|
41
|
+
}: TutorialCarouselProps) {
|
|
42
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
43
|
+
|
|
44
|
+
const nextSlide = () => {
|
|
45
|
+
if (currentSlide < slides.length - 1) {
|
|
46
|
+
setCurrentSlide(currentSlide + 1);
|
|
47
|
+
} else {
|
|
48
|
+
onComplete();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const prevSlide = () => {
|
|
53
|
+
if (currentSlide > 0) {
|
|
54
|
+
setCurrentSlide(currentSlide - 1);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const skipTutorial = () => {
|
|
59
|
+
onComplete();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const currentSlideData = slides[currentSlide];
|
|
63
|
+
const IconComponent = currentSlideData.icon;
|
|
64
|
+
const iconColor = colorMap[currentSlideData.color] || "text-primary";
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
68
|
+
<div className="bg-background rounded-lg p-6 max-w-md w-full mx-4 max-h-[80vh] overflow-hidden">
|
|
69
|
+
{/* Skip button */}
|
|
70
|
+
<div className="flex justify-end mb-4">
|
|
71
|
+
<Button
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="sm"
|
|
74
|
+
onClick={skipTutorial}
|
|
75
|
+
className="text-muted-foreground hover:text-foreground"
|
|
76
|
+
>
|
|
77
|
+
Skip
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Slide content */}
|
|
82
|
+
<div className="text-center space-y-6">
|
|
83
|
+
{/* Icon */}
|
|
84
|
+
<div className="flex justify-center">
|
|
85
|
+
<div className="p-4 rounded-full bg-muted">
|
|
86
|
+
<IconComponent className={`h-12 w-12 ${iconColor}`} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Title */}
|
|
91
|
+
<h2 className="text-2xl font-bold">{currentSlideData.title}</h2>
|
|
92
|
+
|
|
93
|
+
{/* Description */}
|
|
94
|
+
<p className="text-muted-foreground leading-relaxed">
|
|
95
|
+
{currentSlideData.description}
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
{/* Progress dots */}
|
|
99
|
+
<div className="flex justify-center space-x-2 mb-6">
|
|
100
|
+
{slides.map((_, index) => (
|
|
101
|
+
<div
|
|
102
|
+
key={index}
|
|
103
|
+
className={`h-2 w-2 rounded-full transition-colors ${
|
|
104
|
+
index === currentSlide ? "bg-primary" : "bg-muted"
|
|
105
|
+
}`}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Navigation buttons */}
|
|
112
|
+
<div className="flex gap-3">
|
|
113
|
+
<Button
|
|
114
|
+
variant="outline"
|
|
115
|
+
onClick={prevSlide}
|
|
116
|
+
disabled={currentSlide === 0}
|
|
117
|
+
className="flex-1"
|
|
118
|
+
>
|
|
119
|
+
Previous
|
|
120
|
+
</Button>
|
|
121
|
+
<Button onClick={nextSlide} className="flex-1">
|
|
122
|
+
{currentSlide === slides.length - 1 ? "Get Started!" : "Next"}
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|