luxlabs 1.0.0 → 1.0.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.
- package/commands/interface/boilerplate.js +16 -2
- package/package.json +1 -1
- package/templates/interface-boilerplate/.env.example +2 -0
- package/templates/interface-boilerplate/.eslintrc.json +4 -0
- package/templates/interface-boilerplate/app/auth/callback/page.tsx +43 -0
- package/templates/interface-boilerplate/app/dashboard/page.tsx +226 -0
- package/templates/interface-boilerplate/app/globals.css +121 -0
- package/templates/interface-boilerplate/app/layout.tsx +46 -0
- package/templates/interface-boilerplate/app/login/page.tsx +178 -0
- package/templates/interface-boilerplate/app/page.tsx +129 -0
- package/templates/interface-boilerplate/app/signup/page.tsx +199 -0
- package/templates/interface-boilerplate/components/AnalyticsProvider.tsx +142 -0
- package/templates/interface-boilerplate/components/AuthGuard.tsx +76 -0
- package/templates/interface-boilerplate/components/ErrorBoundary.tsx +106 -0
- package/templates/interface-boilerplate/components/theme-provider.tsx +9 -0
- package/templates/interface-boilerplate/components/theme-toggle.tsx +39 -0
- package/templates/interface-boilerplate/components/ui/avatar.tsx +46 -0
- package/templates/interface-boilerplate/components/ui/badge.tsx +32 -0
- package/templates/interface-boilerplate/components/ui/button.tsx +52 -0
- package/templates/interface-boilerplate/components/ui/card.tsx +57 -0
- package/templates/interface-boilerplate/components/ui/checkbox.tsx +27 -0
- package/templates/interface-boilerplate/components/ui/dialog.tsx +100 -0
- package/templates/interface-boilerplate/components/ui/dropdown-menu.tsx +173 -0
- package/templates/interface-boilerplate/components/ui/index.ts +53 -0
- package/templates/interface-boilerplate/components/ui/input.tsx +23 -0
- package/templates/interface-boilerplate/components/ui/label.tsx +20 -0
- package/templates/interface-boilerplate/components/ui/progress.tsx +24 -0
- package/templates/interface-boilerplate/components/ui/select.tsx +149 -0
- package/templates/interface-boilerplate/components/ui/separator.tsx +25 -0
- package/templates/interface-boilerplate/components/ui/skeleton.tsx +12 -0
- package/templates/interface-boilerplate/components/ui/switch.tsx +28 -0
- package/templates/interface-boilerplate/components/ui/tabs.tsx +54 -0
- package/templates/interface-boilerplate/components/ui/textarea.tsx +22 -0
- package/templates/interface-boilerplate/components/ui/tooltip.tsx +29 -0
- package/templates/interface-boilerplate/lib/analytics.ts +182 -0
- package/templates/interface-boilerplate/lib/auth-context.tsx +83 -0
- package/templates/interface-boilerplate/lib/auth.ts +199 -0
- package/templates/interface-boilerplate/lib/callFlow.ts +234 -0
- package/templates/interface-boilerplate/lib/flowTracer.ts +195 -0
- package/templates/interface-boilerplate/lib/hooks/.gitkeep +0 -0
- package/templates/interface-boilerplate/lib/stores/.gitkeep +0 -0
- package/templates/interface-boilerplate/lib/utils.ts +6 -0
- package/templates/interface-boilerplate/next.config.js +6 -0
- package/templates/interface-boilerplate/package.json +51 -0
- package/templates/interface-boilerplate/postcss.config.js +6 -0
- package/templates/interface-boilerplate/tailwind.config.js +103 -0
- package/templates/interface-boilerplate/tsconfig.json +20 -0
|
@@ -3,10 +3,24 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Get the templates directory path
|
|
6
|
-
*
|
|
6
|
+
* Tries multiple paths to support both:
|
|
7
|
+
* - Electron app: electron/bin/lux-cli/commands/interface/ → 4 levels up
|
|
8
|
+
* - npm package: luxlabs/commands/interface/ → 2 levels up
|
|
7
9
|
*/
|
|
8
10
|
function getTemplatesDir() {
|
|
9
|
-
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(__dirname, '../../../../templates/interface-boilerplate'), // Electron app
|
|
13
|
+
path.join(__dirname, '../../templates/interface-boilerplate'), // npm package
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Return first candidate for error message
|
|
23
|
+
return candidates[0];
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { handleOAuthCallback } from '@/lib/auth';
|
|
6
|
+
import { Loader2 } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export default function AuthCallbackPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [error, setError] = useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const result = handleOAuthCallback();
|
|
14
|
+
if (result.success) {
|
|
15
|
+
router.push('/dashboard');
|
|
16
|
+
} else {
|
|
17
|
+
setError(result.error || 'Authentication failed');
|
|
18
|
+
}
|
|
19
|
+
}, [router]);
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
24
|
+
<div className="text-center">
|
|
25
|
+
<h1 className="text-2xl font-bold text-destructive mb-2">Authentication Failed</h1>
|
|
26
|
+
<p className="text-muted-foreground mb-4">{error}</p>
|
|
27
|
+
<a href="/login" className="text-primary hover:underline">
|
|
28
|
+
Back to login
|
|
29
|
+
</a>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
37
|
+
<div className="text-center">
|
|
38
|
+
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
|
|
39
|
+
<p className="text-muted-foreground">Completing sign in...</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AuthGuard } from '@/components/AuthGuard';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { motion } from 'framer-motion';
|
|
7
|
+
import {
|
|
8
|
+
LayoutDashboard,
|
|
9
|
+
Users,
|
|
10
|
+
CreditCard,
|
|
11
|
+
Activity,
|
|
12
|
+
LogOut,
|
|
13
|
+
TrendingUp,
|
|
14
|
+
TrendingDown,
|
|
15
|
+
Menu,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { Button } from '@/components/ui/button';
|
|
18
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
19
|
+
import { Badge } from '@/components/ui/badge';
|
|
20
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
21
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
22
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
23
|
+
import {
|
|
24
|
+
DropdownMenu,
|
|
25
|
+
DropdownMenuContent,
|
|
26
|
+
DropdownMenuItem,
|
|
27
|
+
DropdownMenuLabel,
|
|
28
|
+
DropdownMenuSeparator,
|
|
29
|
+
DropdownMenuTrigger,
|
|
30
|
+
} from '@/components/ui/dropdown-menu';
|
|
31
|
+
import { useAuth } from '@/lib/auth-context';
|
|
32
|
+
|
|
33
|
+
const stats = [
|
|
34
|
+
{ title: 'Total Users', value: '1,234', change: '+12%', trend: 'up', icon: Users },
|
|
35
|
+
{ title: 'Revenue', value: '$45,231', change: '+8%', trend: 'up', icon: CreditCard },
|
|
36
|
+
{ title: 'Active Now', value: '573', change: '-3%', trend: 'down', icon: Activity },
|
|
37
|
+
{ title: 'Growth', value: '23.5%', change: '+5%', trend: 'up', icon: TrendingUp },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function DashboardContent() {
|
|
41
|
+
const [loading, setLoading] = useState(true);
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const { user, signOut } = useAuth();
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
// Simulate loading dashboard data
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}, 500);
|
|
50
|
+
|
|
51
|
+
return () => clearTimeout(timer);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const handleLogout = async () => {
|
|
55
|
+
await signOut();
|
|
56
|
+
router.push('/login');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="min-h-screen bg-background">
|
|
61
|
+
{/* Header */}
|
|
62
|
+
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
63
|
+
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
|
64
|
+
<div className="flex items-center space-x-4">
|
|
65
|
+
<Button variant="ghost" size="icon" className="md:hidden">
|
|
66
|
+
<Menu className="h-5 w-5" />
|
|
67
|
+
</Button>
|
|
68
|
+
<div className="flex items-center space-x-2">
|
|
69
|
+
<LayoutDashboard className="h-6 w-6 text-primary" />
|
|
70
|
+
<span className="text-xl font-semibold">Dashboard</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex items-center space-x-4">
|
|
74
|
+
<ThemeToggle />
|
|
75
|
+
<DropdownMenu>
|
|
76
|
+
<DropdownMenuTrigger asChild>
|
|
77
|
+
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
|
78
|
+
<Avatar className="h-8 w-8">
|
|
79
|
+
<AvatarImage src={user?.image || '/avatar.png'} alt={user?.name || 'User'} />
|
|
80
|
+
<AvatarFallback>{user?.name?.charAt(0).toUpperCase() || 'U'}</AvatarFallback>
|
|
81
|
+
</Avatar>
|
|
82
|
+
</Button>
|
|
83
|
+
</DropdownMenuTrigger>
|
|
84
|
+
<DropdownMenuContent className="w-56" align="end" forceMount>
|
|
85
|
+
<DropdownMenuLabel className="font-normal">
|
|
86
|
+
<div className="flex flex-col space-y-1">
|
|
87
|
+
<p className="text-sm font-medium">{user?.name || 'User'}</p>
|
|
88
|
+
<p className="text-xs text-muted-foreground">{user?.email}</p>
|
|
89
|
+
</div>
|
|
90
|
+
</DropdownMenuLabel>
|
|
91
|
+
<DropdownMenuSeparator />
|
|
92
|
+
<DropdownMenuItem onClick={handleLogout}>
|
|
93
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
94
|
+
Log out
|
|
95
|
+
</DropdownMenuItem>
|
|
96
|
+
</DropdownMenuContent>
|
|
97
|
+
</DropdownMenu>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</header>
|
|
101
|
+
|
|
102
|
+
{/* Main Content */}
|
|
103
|
+
<main className="container mx-auto px-4 py-8">
|
|
104
|
+
<motion.div
|
|
105
|
+
initial={{ opacity: 0, y: 20 }}
|
|
106
|
+
animate={{ opacity: 1, y: 0 }}
|
|
107
|
+
transition={{ duration: 0.4 }}
|
|
108
|
+
>
|
|
109
|
+
<div className="mb-8">
|
|
110
|
+
<h1 className="text-3xl font-bold">Welcome back{user?.name ? `, ${user.name}` : ''}!</h1>
|
|
111
|
+
<p className="text-muted-foreground">
|
|
112
|
+
Here's what's happening with your app today.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Stats Grid */}
|
|
117
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
118
|
+
{stats.map((stat, index) => (
|
|
119
|
+
<motion.div
|
|
120
|
+
key={stat.title}
|
|
121
|
+
initial={{ opacity: 0, y: 20 }}
|
|
122
|
+
animate={{ opacity: 1, y: 0 }}
|
|
123
|
+
transition={{ duration: 0.4, delay: index * 0.1 }}
|
|
124
|
+
>
|
|
125
|
+
<Card>
|
|
126
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
127
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
128
|
+
{stat.title}
|
|
129
|
+
</CardTitle>
|
|
130
|
+
<stat.icon className="h-4 w-4 text-muted-foreground" />
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent>
|
|
133
|
+
{loading ? (
|
|
134
|
+
<Skeleton className="h-8 w-24" />
|
|
135
|
+
) : (
|
|
136
|
+
<>
|
|
137
|
+
<div className="text-2xl font-bold">{stat.value}</div>
|
|
138
|
+
<div className="flex items-center text-xs">
|
|
139
|
+
{stat.trend === 'up' ? (
|
|
140
|
+
<TrendingUp className="mr-1 h-3 w-3 text-green-500" />
|
|
141
|
+
) : (
|
|
142
|
+
<TrendingDown className="mr-1 h-3 w-3 text-red-500" />
|
|
143
|
+
)}
|
|
144
|
+
<span className={stat.trend === 'up' ? 'text-green-500' : 'text-red-500'}>
|
|
145
|
+
{stat.change}
|
|
146
|
+
</span>
|
|
147
|
+
<span className="ml-1 text-muted-foreground">from last month</span>
|
|
148
|
+
</div>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
</motion.div>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Recent Activity */}
|
|
158
|
+
<div className="mt-8">
|
|
159
|
+
<Card>
|
|
160
|
+
<CardHeader>
|
|
161
|
+
<CardTitle>Recent Activity</CardTitle>
|
|
162
|
+
<CardDescription>Your latest actions and updates</CardDescription>
|
|
163
|
+
</CardHeader>
|
|
164
|
+
<CardContent>
|
|
165
|
+
{loading ? (
|
|
166
|
+
<div className="space-y-4">
|
|
167
|
+
{[1, 2, 3].map((i) => (
|
|
168
|
+
<div key={i} className="flex items-center space-x-4">
|
|
169
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
170
|
+
<div className="space-y-2">
|
|
171
|
+
<Skeleton className="h-4 w-48" />
|
|
172
|
+
<Skeleton className="h-3 w-24" />
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="space-y-4">
|
|
179
|
+
<div className="flex items-center space-x-4">
|
|
180
|
+
<Avatar>
|
|
181
|
+
<AvatarFallback>US</AvatarFallback>
|
|
182
|
+
</Avatar>
|
|
183
|
+
<div className="flex-1">
|
|
184
|
+
<p className="text-sm font-medium">New user signed up</p>
|
|
185
|
+
<p className="text-xs text-muted-foreground">2 minutes ago</p>
|
|
186
|
+
</div>
|
|
187
|
+
<Badge>New</Badge>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex items-center space-x-4">
|
|
190
|
+
<Avatar>
|
|
191
|
+
<AvatarFallback>OR</AvatarFallback>
|
|
192
|
+
</Avatar>
|
|
193
|
+
<div className="flex-1">
|
|
194
|
+
<p className="text-sm font-medium">Order #1234 completed</p>
|
|
195
|
+
<p className="text-xs text-muted-foreground">1 hour ago</p>
|
|
196
|
+
</div>
|
|
197
|
+
<Badge variant="success">Completed</Badge>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="flex items-center space-x-4">
|
|
200
|
+
<Avatar>
|
|
201
|
+
<AvatarFallback>SY</AvatarFallback>
|
|
202
|
+
</Avatar>
|
|
203
|
+
<div className="flex-1">
|
|
204
|
+
<p className="text-sm font-medium">System update deployed</p>
|
|
205
|
+
<p className="text-xs text-muted-foreground">3 hours ago</p>
|
|
206
|
+
</div>
|
|
207
|
+
<Badge variant="secondary">System</Badge>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</CardContent>
|
|
212
|
+
</Card>
|
|
213
|
+
</div>
|
|
214
|
+
</motion.div>
|
|
215
|
+
</main>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default function DashboardPage() {
|
|
221
|
+
return (
|
|
222
|
+
<AuthGuard>
|
|
223
|
+
<DashboardContent />
|
|
224
|
+
</AuthGuard>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 221.2 83.2% 53.3%;
|
|
14
|
+
--primary-foreground: 210 40% 98%;
|
|
15
|
+
--secondary: 210 40% 96.1%;
|
|
16
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
+
--muted: 210 40% 96.1%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96.1%;
|
|
20
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--success: 142.1 76.2% 36.3%;
|
|
24
|
+
--success-foreground: 355.7 100% 97.3%;
|
|
25
|
+
--warning: 38 92% 50%;
|
|
26
|
+
--warning-foreground: 48 96% 89%;
|
|
27
|
+
--border: 214.3 31.8% 91.4%;
|
|
28
|
+
--input: 214.3 31.8% 91.4%;
|
|
29
|
+
--ring: 221.2 83.2% 53.3%;
|
|
30
|
+
--radius: 0.5rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dark {
|
|
34
|
+
--background: 222.2 84% 4.9%;
|
|
35
|
+
--foreground: 210 40% 98%;
|
|
36
|
+
--card: 222.2 84% 4.9%;
|
|
37
|
+
--card-foreground: 210 40% 98%;
|
|
38
|
+
--popover: 222.2 84% 4.9%;
|
|
39
|
+
--popover-foreground: 210 40% 98%;
|
|
40
|
+
--primary: 217.2 91.2% 59.8%;
|
|
41
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
42
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
43
|
+
--secondary-foreground: 210 40% 98%;
|
|
44
|
+
--muted: 217.2 32.6% 17.5%;
|
|
45
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
46
|
+
--accent: 217.2 32.6% 17.5%;
|
|
47
|
+
--accent-foreground: 210 40% 98%;
|
|
48
|
+
--destructive: 0 62.8% 30.6%;
|
|
49
|
+
--destructive-foreground: 210 40% 98%;
|
|
50
|
+
--success: 142.1 70.6% 45.3%;
|
|
51
|
+
--success-foreground: 144.9 80.4% 10%;
|
|
52
|
+
--warning: 32 95% 44%;
|
|
53
|
+
--warning-foreground: 48 96% 89%;
|
|
54
|
+
--border: 217.2 32.6% 17.5%;
|
|
55
|
+
--input: 217.2 32.6% 17.5%;
|
|
56
|
+
--ring: 224.3 76.3% 48%;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@layer base {
|
|
61
|
+
* {
|
|
62
|
+
@apply border-border;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
body {
|
|
66
|
+
@apply bg-background text-foreground;
|
|
67
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Smooth scrolling */
|
|
71
|
+
html {
|
|
72
|
+
scroll-behavior: smooth;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Better focus styles */
|
|
76
|
+
:focus-visible {
|
|
77
|
+
@apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Reduced motion */
|
|
81
|
+
@media (prefers-reduced-motion: reduce) {
|
|
82
|
+
*,
|
|
83
|
+
::before,
|
|
84
|
+
::after {
|
|
85
|
+
animation-duration: 0.01ms !important;
|
|
86
|
+
animation-iteration-count: 1 !important;
|
|
87
|
+
transition-duration: 0.01ms !important;
|
|
88
|
+
scroll-behavior: auto !important;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@layer utilities {
|
|
94
|
+
/* Hide scrollbar but keep functionality */
|
|
95
|
+
.scrollbar-hide {
|
|
96
|
+
-ms-overflow-style: none;
|
|
97
|
+
scrollbar-width: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Text balance for headings */
|
|
105
|
+
.text-balance {
|
|
106
|
+
text-wrap: balance;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Animation delay utilities */
|
|
110
|
+
.animation-delay-150 {
|
|
111
|
+
animation-delay: 150ms;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.animation-delay-300 {
|
|
115
|
+
animation-delay: 300ms;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.animation-delay-500 {
|
|
119
|
+
animation-delay: 500ms;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
import { ThemeProvider } from '@/components/theme-provider';
|
|
5
|
+
import { AuthProvider } from '@/lib/auth-context';
|
|
6
|
+
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
7
|
+
import { AnalyticsProvider } from '@/components/AnalyticsProvider';
|
|
8
|
+
|
|
9
|
+
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = {
|
|
12
|
+
title: 'My App',
|
|
13
|
+
description: 'Built with Lux',
|
|
14
|
+
openGraph: {
|
|
15
|
+
title: 'My App',
|
|
16
|
+
description: 'Built with Lux',
|
|
17
|
+
type: 'website',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en" suppressHydrationWarning>
|
|
28
|
+
<body className={inter.variable}>
|
|
29
|
+
<ThemeProvider
|
|
30
|
+
attribute="class"
|
|
31
|
+
defaultTheme="system"
|
|
32
|
+
enableSystem
|
|
33
|
+
disableTransitionOnChange
|
|
34
|
+
>
|
|
35
|
+
<ErrorBoundary>
|
|
36
|
+
<AnalyticsProvider>
|
|
37
|
+
<AuthProvider>
|
|
38
|
+
{children}
|
|
39
|
+
</AuthProvider>
|
|
40
|
+
</AnalyticsProvider>
|
|
41
|
+
</ErrorBoundary>
|
|
42
|
+
</ThemeProvider>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { motion } from 'framer-motion';
|
|
6
|
+
import { Loader2, Mail, Lock, ArrowLeft } from 'lucide-react';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { Input } from '@/components/ui/input';
|
|
9
|
+
import { Label } from '@/components/ui/label';
|
|
10
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
11
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
12
|
+
import { useAuth } from '@/lib/auth-context';
|
|
13
|
+
|
|
14
|
+
export default function LoginPage() {
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const { signIn, signInWithGoogle } = useAuth();
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setError('');
|
|
25
|
+
setLoading(true);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await signIn(email, password);
|
|
29
|
+
|
|
30
|
+
if (result.success) {
|
|
31
|
+
router.push('/dashboard');
|
|
32
|
+
} else {
|
|
33
|
+
setError(result.error || 'Invalid email or password');
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError('An error occurred. Please try again.');
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="min-h-screen flex flex-col bg-background">
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<header className="flex items-center justify-between p-4">
|
|
46
|
+
<Button variant="ghost" size="sm" asChild>
|
|
47
|
+
<a href="/">
|
|
48
|
+
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
49
|
+
Back
|
|
50
|
+
</a>
|
|
51
|
+
</Button>
|
|
52
|
+
<ThemeToggle />
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
{/* Login Form */}
|
|
56
|
+
<main className="flex-1 flex items-center justify-center p-4">
|
|
57
|
+
<motion.div
|
|
58
|
+
initial={{ opacity: 0, y: 20 }}
|
|
59
|
+
animate={{ opacity: 1, y: 0 }}
|
|
60
|
+
transition={{ duration: 0.4 }}
|
|
61
|
+
className="w-full max-w-md"
|
|
62
|
+
>
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader className="text-center">
|
|
65
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
66
|
+
<Lock className="h-6 w-6 text-primary" />
|
|
67
|
+
</div>
|
|
68
|
+
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
|
69
|
+
<CardDescription>
|
|
70
|
+
Enter your credentials to access your account
|
|
71
|
+
</CardDescription>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<form onSubmit={handleSubmit}>
|
|
74
|
+
<CardContent className="space-y-4">
|
|
75
|
+
{error && (
|
|
76
|
+
<motion.div
|
|
77
|
+
initial={{ opacity: 0, height: 0 }}
|
|
78
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
79
|
+
className="rounded-lg bg-destructive/10 p-3 text-sm text-destructive"
|
|
80
|
+
>
|
|
81
|
+
{error}
|
|
82
|
+
</motion.div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div className="space-y-2">
|
|
86
|
+
<Label htmlFor="email">Email</Label>
|
|
87
|
+
<div className="relative">
|
|
88
|
+
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
89
|
+
<Input
|
|
90
|
+
id="email"
|
|
91
|
+
type="email"
|
|
92
|
+
placeholder="name@example.com"
|
|
93
|
+
value={email}
|
|
94
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
95
|
+
className="pl-10"
|
|
96
|
+
required
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<Label htmlFor="password">Password</Label>
|
|
103
|
+
<div className="relative">
|
|
104
|
+
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
105
|
+
<Input
|
|
106
|
+
id="password"
|
|
107
|
+
type="password"
|
|
108
|
+
placeholder="••••••••"
|
|
109
|
+
value={password}
|
|
110
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
111
|
+
className="pl-10"
|
|
112
|
+
required
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</CardContent>
|
|
117
|
+
<CardFooter className="flex flex-col space-y-4">
|
|
118
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
119
|
+
{loading ? (
|
|
120
|
+
<>
|
|
121
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
122
|
+
Signing in...
|
|
123
|
+
</>
|
|
124
|
+
) : (
|
|
125
|
+
'Sign in'
|
|
126
|
+
)}
|
|
127
|
+
</Button>
|
|
128
|
+
|
|
129
|
+
<div className="relative w-full">
|
|
130
|
+
<div className="absolute inset-0 flex items-center">
|
|
131
|
+
<span className="w-full border-t" />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
134
|
+
<span className="bg-background px-2 text-muted-foreground">Or continue with</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
variant="outline"
|
|
141
|
+
className="w-full"
|
|
142
|
+
onClick={() => signInWithGoogle()}
|
|
143
|
+
>
|
|
144
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
145
|
+
<path
|
|
146
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
147
|
+
fill="#4285F4"
|
|
148
|
+
/>
|
|
149
|
+
<path
|
|
150
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
151
|
+
fill="#34A853"
|
|
152
|
+
/>
|
|
153
|
+
<path
|
|
154
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
155
|
+
fill="#FBBC05"
|
|
156
|
+
/>
|
|
157
|
+
<path
|
|
158
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
159
|
+
fill="#EA4335"
|
|
160
|
+
/>
|
|
161
|
+
</svg>
|
|
162
|
+
Continue with Google
|
|
163
|
+
</Button>
|
|
164
|
+
|
|
165
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
166
|
+
Don't have an account?{' '}
|
|
167
|
+
<a href="/signup" className="text-primary hover:underline">
|
|
168
|
+
Sign up
|
|
169
|
+
</a>
|
|
170
|
+
</p>
|
|
171
|
+
</CardFooter>
|
|
172
|
+
</form>
|
|
173
|
+
</Card>
|
|
174
|
+
</motion.div>
|
|
175
|
+
</main>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|