r9stack 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +217 -0
  3. package/dist/commands/init.d.ts +10 -0
  4. package/dist/commands/init.d.ts.map +1 -0
  5. package/dist/commands/init.js +239 -0
  6. package/dist/commands/init.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +65 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/payload/assets/.gitkeep +0 -0
  12. package/dist/payload/assets/favicon.ico +0 -0
  13. package/dist/payload/assets/images/r9stack-logo-markonly-circle.png +0 -0
  14. package/dist/payload/assets/images/r9stack-logo-markonly-whitebg.png +0 -0
  15. package/dist/payload/assets/images/r9stack-logo-markonly.png +0 -0
  16. package/dist/payload/assets/images/r9stack-logo.png +0 -0
  17. package/dist/payload/assets/logo192.png +0 -0
  18. package/dist/payload/assets/logo512.png +0 -0
  19. package/dist/payload/assets/manifest.json +26 -0
  20. package/dist/payload/assets/robots.txt +3 -0
  21. package/dist/payload/templates/.gitkeep +0 -0
  22. package/dist/payload/templates/config/components.json +25 -0
  23. package/dist/payload/templates/config/env.example +14 -0
  24. package/dist/payload/templates/config/tsconfig.json +26 -0
  25. package/dist/payload/templates/config/vite.config.ts +23 -0
  26. package/dist/payload/templates/convex/auth.config.ts +7 -0
  27. package/dist/payload/templates/convex/messages.ts +28 -0
  28. package/dist/payload/templates/convex/schema.ts +24 -0
  29. package/dist/payload/templates/convex/tsconfig.json +21 -0
  30. package/dist/payload/templates/src/components/AppShell.tsx +21 -0
  31. package/dist/payload/templates/src/components/AuthProvider.tsx +50 -0
  32. package/dist/payload/templates/src/components/ConvexClientProvider.tsx +20 -0
  33. package/dist/payload/templates/src/components/NavGroup.tsx +46 -0
  34. package/dist/payload/templates/src/components/NavItem.tsx +36 -0
  35. package/dist/payload/templates/src/components/Sidebar.tsx +76 -0
  36. package/dist/payload/templates/src/components/UserMenu.tsx +102 -0
  37. package/dist/payload/templates/src/components/ui/button.tsx +59 -0
  38. package/dist/payload/templates/src/lib/auth-client.ts +29 -0
  39. package/dist/payload/templates/src/lib/auth-server.ts +97 -0
  40. package/dist/payload/templates/src/lib/auth.ts +15 -0
  41. package/dist/payload/templates/src/lib/utils.ts +7 -0
  42. package/dist/payload/templates/src/router.tsx +18 -0
  43. package/dist/payload/templates/src/routes/__root.tsx +53 -0
  44. package/dist/payload/templates/src/routes/app/demo/convex.messages.tsx +66 -0
  45. package/dist/payload/templates/src/routes/app/index.tsx +20 -0
  46. package/dist/payload/templates/src/routes/app/route.tsx +23 -0
  47. package/dist/payload/templates/src/routes/auth/callback.tsx +36 -0
  48. package/dist/payload/templates/src/routes/auth/sign-in.tsx +22 -0
  49. package/dist/payload/templates/src/routes/auth/sign-out.tsx +22 -0
  50. package/dist/payload/templates/src/routes/index.tsx +85 -0
  51. package/dist/payload/templates/src/styles.css +141 -0
  52. package/dist/utils/exec.d.ts +17 -0
  53. package/dist/utils/exec.d.ts.map +1 -0
  54. package/dist/utils/exec.js +49 -0
  55. package/dist/utils/exec.js.map +1 -0
  56. package/dist/utils/flight-rules.d.ts +5 -0
  57. package/dist/utils/flight-rules.d.ts.map +1 -0
  58. package/dist/utils/flight-rules.js +23 -0
  59. package/dist/utils/flight-rules.js.map +1 -0
  60. package/dist/utils/github.d.ts +17 -0
  61. package/dist/utils/github.d.ts.map +1 -0
  62. package/dist/utils/github.js +64 -0
  63. package/dist/utils/github.js.map +1 -0
  64. package/dist/utils/logger.d.ts +10 -0
  65. package/dist/utils/logger.d.ts.map +1 -0
  66. package/dist/utils/logger.js +27 -0
  67. package/dist/utils/logger.js.map +1 -0
  68. package/dist/utils/starters.d.ts +20 -0
  69. package/dist/utils/starters.d.ts.map +1 -0
  70. package/dist/utils/starters.js +43 -0
  71. package/dist/utils/starters.js.map +1 -0
  72. package/dist/utils/templates.d.ts +12 -0
  73. package/dist/utils/templates.d.ts.map +1 -0
  74. package/dist/utils/templates.js +77 -0
  75. package/dist/utils/templates.js.map +1 -0
  76. package/package.json +46 -0
@@ -0,0 +1,59 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean
46
+ }) {
47
+ const Comp = asChild ? Slot : "button"
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ export { Button, buttonVariants }
59
+
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { User } from './auth'
3
+
4
+ export interface AuthContextValue {
5
+ user: User | null
6
+ isAuthenticated: boolean
7
+ isLoading: boolean
8
+ signIn: () => void
9
+ signOut: () => void
10
+ }
11
+
12
+ export const AuthContext = createContext<AuthContextValue | null>(null)
13
+
14
+ export function useAuth(): AuthContextValue {
15
+ const context = useContext(AuthContext)
16
+ if (!context) {
17
+ throw new Error('useAuth must be used within an AuthProvider')
18
+ }
19
+ return context
20
+ }
21
+
22
+ export function signIn() {
23
+ window.location.href = '/auth/sign-in'
24
+ }
25
+
26
+ export function signOut() {
27
+ window.location.href = '/auth/sign-out'
28
+ }
29
+
@@ -0,0 +1,97 @@
1
+ import { createServerFn } from '@tanstack/react-start'
2
+ import { getRequest, setCookie, deleteCookie } from '@tanstack/react-start/server'
3
+ import { getIronSession } from 'iron-session'
4
+ import { WorkOS } from '@workos-inc/node'
5
+ import type { SessionData, User } from './auth'
6
+
7
+ const workos = new WorkOS(process.env.WORKOS_API_KEY)
8
+ const clientId = process.env.WORKOS_CLIENT_ID!
9
+
10
+ const sessionOptions = {
11
+ password: process.env.WORKOS_COOKIE_PASSWORD!,
12
+ cookieName: 'r9_session',
13
+ cookieOptions: {
14
+ secure: process.env.NODE_ENV === 'production',
15
+ httpOnly: true,
16
+ sameSite: 'lax' as const,
17
+ },
18
+ }
19
+
20
+ export const getAuthUrl = createServerFn({ method: 'GET' }).handler(async () => {
21
+ const authorizationUrl = workos.userManagement.getAuthorizationUrl({
22
+ provider: 'authkit',
23
+ clientId,
24
+ redirectUri: process.env.WORKOS_REDIRECT_URI!,
25
+ })
26
+ return authorizationUrl
27
+ })
28
+
29
+ export const getCurrentUser = createServerFn({ method: 'GET' }).handler(
30
+ async (): Promise<User | null> => {
31
+ const request = getRequest()
32
+ if (!request) return null
33
+
34
+ const response = new Response()
35
+ const session = await getIronSession<SessionData>(request, response, sessionOptions)
36
+
37
+ if (!session.user) return null
38
+ if (session.expiresAt && Date.now() > session.expiresAt) return null
39
+
40
+ return session.user
41
+ }
42
+ )
43
+
44
+ export const handleAuthCallback = createServerFn({ method: 'GET' }).handler(
45
+ async (ctx: { data: { code: string } }): Promise<User> => {
46
+ const { code } = ctx.data
47
+
48
+ const authResponse = await workos.userManagement.authenticateWithCode({
49
+ clientId,
50
+ code,
51
+ })
52
+
53
+ const user: User = {
54
+ id: authResponse.user.id,
55
+ email: authResponse.user.email,
56
+ firstName: authResponse.user.firstName,
57
+ lastName: authResponse.user.lastName,
58
+ profilePictureUrl: authResponse.user.profilePictureUrl,
59
+ }
60
+
61
+ const request = getRequest()
62
+ if (!request) throw new Error('No request available')
63
+
64
+ const response = new Response()
65
+ const session = await getIronSession<SessionData>(request, response, sessionOptions)
66
+
67
+ session.user = user
68
+ session.accessToken = authResponse.accessToken
69
+ session.refreshToken = authResponse.refreshToken
70
+ session.expiresAt = Date.now() + 60 * 60 * 1000 // 1 hour
71
+
72
+ await session.save()
73
+
74
+ const cookieHeader = response.headers.get('Set-Cookie')
75
+ if (cookieHeader) {
76
+ const [nameValue] = cookieHeader.split(';')
77
+ const [, value] = nameValue.split('=')
78
+ setCookie('r9_session', value, {
79
+ httpOnly: true,
80
+ secure: process.env.NODE_ENV === 'production',
81
+ sameSite: 'lax',
82
+ maxAge: 60 * 60,
83
+ path: '/',
84
+ })
85
+ }
86
+
87
+ return user
88
+ }
89
+ )
90
+
91
+ export const signOutServer = createServerFn({ method: 'POST' }).handler(
92
+ async () => {
93
+ deleteCookie('r9_session')
94
+ return { success: true }
95
+ }
96
+ )
97
+
@@ -0,0 +1,15 @@
1
+ export interface User {
2
+ id: string
3
+ email: string
4
+ firstName: string | null
5
+ lastName: string | null
6
+ profilePictureUrl: string | null
7
+ }
8
+
9
+ export interface SessionData {
10
+ user?: User
11
+ accessToken?: string
12
+ refreshToken?: string
13
+ expiresAt?: number
14
+ }
15
+
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
@@ -0,0 +1,18 @@
1
+ import { createRouter as createTanStackRouter } from '@tanstack/react-router'
2
+ import { routeTree } from './routeTree.gen'
3
+
4
+ export function createRouter() {
5
+ const router = createTanStackRouter({
6
+ routeTree,
7
+ scrollRestoration: true,
8
+ })
9
+
10
+ return router
11
+ }
12
+
13
+ declare module '@tanstack/react-router' {
14
+ interface Register {
15
+ router: ReturnType<typeof createRouter>
16
+ }
17
+ }
18
+
@@ -0,0 +1,53 @@
1
+ import { HeadContent, Scripts, createRootRoute, Outlet } from '@tanstack/react-router'
2
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
3
+ import { TanStackDevtools } from '@tanstack/react-devtools'
4
+
5
+ import { AuthProvider } from '../components/AuthProvider'
6
+ import { ConvexClientProvider } from '../components/ConvexClientProvider'
7
+
8
+ import appCss from '../styles.css?url'
9
+
10
+ export const Route = createRootRoute({
11
+ head: () => ({
12
+ meta: [
13
+ { charSet: 'utf-8' },
14
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
15
+ { title: '{{PROJECT_NAME}}' },
16
+ { property: 'og:type', content: 'website' },
17
+ { property: 'og:title', content: '{{PROJECT_NAME}}' },
18
+ { property: 'og:description', content: 'Built with r9stack' },
19
+ { property: 'og:image', content: '/images/logo.png' },
20
+ { name: 'twitter:card', content: 'summary' },
21
+ { name: 'twitter:title', content: '{{PROJECT_NAME}}' },
22
+ { name: 'twitter:description', content: 'Built with r9stack' },
23
+ { name: 'twitter:image', content: '/images/logo.png' },
24
+ ],
25
+ links: [{ rel: 'stylesheet', href: appCss }],
26
+ }),
27
+ shellComponent: RootDocument,
28
+ })
29
+
30
+ function RootDocument({ children }: { children: React.ReactNode }) {
31
+ return (
32
+ <html lang="en">
33
+ <head>
34
+ <HeadContent />
35
+ </head>
36
+ <body>
37
+ <AuthProvider>
38
+ <ConvexClientProvider>
39
+ {children}
40
+ </ConvexClientProvider>
41
+ </AuthProvider>
42
+ <TanStackDevtools
43
+ config={{ position: 'bottom-right' }}
44
+ plugins={[
45
+ { name: 'Tanstack Router', render: <TanStackRouterDevtoolsPanel /> },
46
+ ]}
47
+ />
48
+ <Scripts />
49
+ </body>
50
+ </html>
51
+ )
52
+ }
53
+
@@ -0,0 +1,66 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { useQuery, useMutation } from 'convex/react'
3
+ import { api } from '../../../../convex/_generated/api'
4
+ import { useState } from 'react'
5
+ import { Button } from '../../../components/ui/button'
6
+
7
+ export const Route = createFileRoute('/app/demo/convex/messages')({
8
+ component: MessagesDemo,
9
+ })
10
+
11
+ function MessagesDemo() {
12
+ const messages = useQuery(api.messages.list)
13
+ const sendMessage = useMutation(api.messages.send)
14
+ const [newMessage, setNewMessage] = useState('')
15
+
16
+ const handleSubmit = async (e: React.FormEvent) => {
17
+ e.preventDefault()
18
+ if (!newMessage.trim()) return
19
+
20
+ await sendMessage({ text: newMessage.trim() })
21
+ setNewMessage('')
22
+ }
23
+
24
+ return (
25
+ <div className="p-8 max-w-2xl">
26
+ <h1 className="text-2xl font-bold text-foreground mb-2">
27
+ Convex Messages Demo
28
+ </h1>
29
+ <p className="text-muted-foreground mb-6">
30
+ Real-time messages powered by Convex. Open in multiple tabs to see sync!
31
+ </p>
32
+
33
+ <form onSubmit={handleSubmit} className="flex gap-2 mb-6">
34
+ <input
35
+ type="text"
36
+ value={newMessage}
37
+ onChange={(e) => setNewMessage(e.target.value)}
38
+ placeholder="Type a message..."
39
+ className="flex-1 px-3 py-2 border border-input rounded-md bg-background text-foreground"
40
+ />
41
+ <Button type="submit">Send</Button>
42
+ </form>
43
+
44
+ <div className="space-y-2">
45
+ {messages === undefined ? (
46
+ <p className="text-muted-foreground">Loading...</p>
47
+ ) : messages.length === 0 ? (
48
+ <p className="text-muted-foreground">No messages yet. Send one!</p>
49
+ ) : (
50
+ messages.map((message) => (
51
+ <div
52
+ key={message._id}
53
+ className="p-3 bg-muted rounded-md"
54
+ >
55
+ <p className="text-foreground">{message.text}</p>
56
+ <p className="text-xs text-muted-foreground mt-1">
57
+ {new Date(message.createdAt).toLocaleString()}
58
+ </p>
59
+ </div>
60
+ ))
61
+ )}
62
+ </div>
63
+ </div>
64
+ )
65
+ }
66
+
@@ -0,0 +1,20 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { useAuth } from '../../lib/auth-client'
3
+
4
+ export const Route = createFileRoute('/app/')({ component: AppHome })
5
+
6
+ function AppHome() {
7
+ const { user } = useAuth()
8
+
9
+ return (
10
+ <div className="p-8">
11
+ <h1 className="text-3xl font-bold text-foreground mb-4">
12
+ Welcome{user?.firstName ? `, ${user.firstName}` : ''}!
13
+ </h1>
14
+ <p className="text-muted-foreground">
15
+ You're now in the authenticated area of your app.
16
+ </p>
17
+ </div>
18
+ )
19
+ }
20
+
@@ -0,0 +1,23 @@
1
+ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2
+ import { getCurrentUser } from '../../lib/auth-server'
3
+ import { AppShell } from '../../components/AppShell'
4
+
5
+ export const Route = createFileRoute('/app')({
6
+ beforeLoad: async () => {
7
+ const user = await getCurrentUser()
8
+ if (!user) {
9
+ throw redirect({ to: '/' })
10
+ }
11
+ return {}
12
+ },
13
+ component: AppLayout,
14
+ })
15
+
16
+ function AppLayout() {
17
+ return (
18
+ <AppShell>
19
+ <Outlet />
20
+ </AppShell>
21
+ )
22
+ }
23
+
@@ -0,0 +1,36 @@
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
2
+ import { handleAuthCallback } from '../../lib/auth-server'
3
+
4
+ export const Route = createFileRoute('/auth/callback')({
5
+ beforeLoad: async ({ search }) => {
6
+ const code = (search as { code?: string }).code
7
+
8
+ if (!code) {
9
+ throw redirect({ to: '/', search: { error: 'no_code' } })
10
+ }
11
+
12
+ try {
13
+ await handleAuthCallback({ data: { code } })
14
+ throw redirect({ to: '/app' })
15
+ } catch (error) {
16
+ if (error instanceof Response || (error as { to?: string })?.to) {
17
+ throw error
18
+ }
19
+ console.error('Auth callback error:', error)
20
+ throw redirect({ to: '/', search: { error: 'auth_failed' } })
21
+ }
22
+ },
23
+ component: CallbackPage,
24
+ })
25
+
26
+ function CallbackPage() {
27
+ return (
28
+ <div className="flex items-center justify-center min-h-screen">
29
+ <div className="text-center">
30
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
31
+ <p className="text-muted-foreground">Completing sign in...</p>
32
+ </div>
33
+ </div>
34
+ )
35
+ }
36
+
@@ -0,0 +1,22 @@
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
2
+ import { getAuthUrl } from '../../lib/auth-server'
3
+
4
+ export const Route = createFileRoute('/auth/sign-in')({
5
+ beforeLoad: async () => {
6
+ const authUrl = await getAuthUrl()
7
+ throw redirect({ href: authUrl })
8
+ },
9
+ component: SignInPage,
10
+ })
11
+
12
+ function SignInPage() {
13
+ return (
14
+ <div className="flex items-center justify-center min-h-screen">
15
+ <div className="text-center">
16
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
17
+ <p className="text-muted-foreground">Redirecting to sign in...</p>
18
+ </div>
19
+ </div>
20
+ )
21
+ }
22
+
@@ -0,0 +1,22 @@
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
2
+ import { signOutServer } from '../../lib/auth-server'
3
+
4
+ export const Route = createFileRoute('/auth/sign-out')({
5
+ beforeLoad: async () => {
6
+ await signOutServer()
7
+ throw redirect({ to: '/' })
8
+ },
9
+ component: SignOutPage,
10
+ })
11
+
12
+ function SignOutPage() {
13
+ return (
14
+ <div className="flex items-center justify-center min-h-screen">
15
+ <div className="text-center">
16
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
17
+ <p className="text-muted-foreground">Signing out...</p>
18
+ </div>
19
+ </div>
20
+ )
21
+ }
22
+
@@ -0,0 +1,85 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+ import { useAuth } from '../lib/auth-client'
3
+ import { Button } from '../components/ui/button'
4
+
5
+ export const Route = createFileRoute('/')({ component: LandingPage })
6
+
7
+ function LandingPage() {
8
+ const { user, isAuthenticated, isLoading, signIn } = useAuth()
9
+
10
+ return (
11
+ <div className="min-h-screen bg-background">
12
+ {/* Header */}
13
+ <header className="border-b border-border">
14
+ <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
15
+ <div className="flex items-center gap-3">
16
+ <span className="text-xl font-semibold text-foreground">
17
+ {{PROJECT_NAME}}
18
+ </span>
19
+ </div>
20
+
21
+ <div className="flex items-center gap-3">
22
+ {isLoading ? (
23
+ <div className="w-20 h-9 bg-muted animate-pulse rounded-md" />
24
+ ) : isAuthenticated ? (
25
+ <Link to="/app">
26
+ <Button>Go to App</Button>
27
+ </Link>
28
+ ) : (
29
+ <>
30
+ <Button variant="ghost" onClick={signIn}>
31
+ Sign In
32
+ </Button>
33
+ <Button onClick={signIn}>Get Started</Button>
34
+ </>
35
+ )}
36
+ </div>
37
+ </div>
38
+ </header>
39
+
40
+ {/* Hero Section */}
41
+ <main className="max-w-6xl mx-auto px-6 py-20">
42
+ <div className="text-center max-w-3xl mx-auto">
43
+ <h1 className="text-5xl font-bold tracking-tight text-foreground mb-6">
44
+ Welcome to {{PROJECT_NAME}}
45
+ </h1>
46
+ <p className="text-xl text-muted-foreground mb-10">
47
+ Built with r9stack — TanStack Start, Convex, WorkOS, and shadcn/ui
48
+ pre-integrated.
49
+ </p>
50
+
51
+ <div className="flex items-center justify-center gap-4 mb-16">
52
+ {isAuthenticated ? (
53
+ <Link to="/app">
54
+ <Button size="lg" className="px-8">
55
+ Open App
56
+ </Button>
57
+ </Link>
58
+ ) : (
59
+ <>
60
+ <Button size="lg" className="px-8" onClick={signIn}>
61
+ Get Started Free
62
+ </Button>
63
+ <Button size="lg" variant="outline" onClick={signIn}>
64
+ Sign In
65
+ </Button>
66
+ </>
67
+ )}
68
+ </div>
69
+
70
+ {isAuthenticated && user && (
71
+ <div className="mb-16 p-4 bg-muted/50 rounded-lg border border-border inline-block">
72
+ <p className="text-sm text-muted-foreground">
73
+ Signed in as{' '}
74
+ <span className="font-medium text-foreground">
75
+ {user.email}
76
+ </span>
77
+ </p>
78
+ </div>
79
+ )}
80
+ </div>
81
+ </main>
82
+ </div>
83
+ )
84
+ }
85
+