create-workerstack 0.1.1 → 0.1.3
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/bin/index.js +4 -3
- package/package.json +1 -1
- package/template/package.json +3 -0
- package/template/src/client/App.tsx +26 -308
- package/template/src/client/components/AppNavbar.tsx +100 -0
- package/template/src/client/components/AuthLayout.tsx +111 -0
- package/template/src/client/components/ProtectedRoute.tsx +22 -0
- package/template/src/client/components/ThemeToggle.tsx +30 -0
- package/template/src/client/lib/auth-client.ts +17 -0
- package/template/src/client/pages/ChangePasswordPage.tsx +113 -0
- package/template/src/client/pages/DashboardPage.tsx +101 -0
- package/template/src/client/pages/ForgotPasswordPage.tsx +87 -0
- package/template/src/client/pages/HomePage.tsx +294 -0
- package/template/src/client/pages/LoginPage.tsx +108 -0
- package/template/src/client/pages/NotFoundPage.tsx +80 -0
- package/template/src/client/pages/RegisterPage.tsx +123 -0
- package/template/src/client/pages/ResetPasswordPage.tsx +143 -0
- package/template/src/client/theme.ts +30 -0
- package/template/src/style.css +0 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { TextField, Button, Alert, Typography, Box, CircularProgress } from '@mui/material';
|
|
3
|
+
import { Link } from 'react-router';
|
|
4
|
+
import { authClient } from '@/client/lib/auth-client';
|
|
5
|
+
import { AuthLayout } from '../components/AuthLayout';
|
|
6
|
+
|
|
7
|
+
export default function ChangePasswordPage() {
|
|
8
|
+
const [currentPassword, setCurrentPassword] = useState('');
|
|
9
|
+
const [newPassword, setNewPassword] = useState('');
|
|
10
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
11
|
+
const [error, setError] = useState('');
|
|
12
|
+
const [success, setSuccess] = useState(false);
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setError('');
|
|
18
|
+
|
|
19
|
+
if (newPassword !== confirmPassword) {
|
|
20
|
+
setError('Passwords do not match');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setLoading(true);
|
|
25
|
+
|
|
26
|
+
const { error } = await authClient.changePassword({
|
|
27
|
+
currentPassword,
|
|
28
|
+
newPassword,
|
|
29
|
+
revokeOtherSessions: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
setLoading(false);
|
|
33
|
+
|
|
34
|
+
if (error) {
|
|
35
|
+
setError(error.message);
|
|
36
|
+
} else {
|
|
37
|
+
setSuccess(true);
|
|
38
|
+
setCurrentPassword('');
|
|
39
|
+
setNewPassword('');
|
|
40
|
+
setConfirmPassword('');
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<AuthLayout
|
|
46
|
+
title="Change password"
|
|
47
|
+
subtitle="Update your account password"
|
|
48
|
+
footer={
|
|
49
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
50
|
+
<Typography
|
|
51
|
+
component={Link}
|
|
52
|
+
to="/dashboard"
|
|
53
|
+
variant="body2"
|
|
54
|
+
sx={{ color: 'primary.main', textDecoration: 'none', fontWeight: 600, '&:hover': { textDecoration: 'underline' } }}
|
|
55
|
+
>
|
|
56
|
+
Back to dashboard
|
|
57
|
+
</Typography>
|
|
58
|
+
</Typography>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
62
|
+
{error && <Alert severity="error">{error}</Alert>}
|
|
63
|
+
{success && <Alert severity="success">Password changed successfully.</Alert>}
|
|
64
|
+
|
|
65
|
+
<TextField
|
|
66
|
+
label="Current Password"
|
|
67
|
+
type="password"
|
|
68
|
+
value={currentPassword}
|
|
69
|
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
70
|
+
required
|
|
71
|
+
fullWidth
|
|
72
|
+
autoComplete="current-password"
|
|
73
|
+
autoFocus
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<TextField
|
|
77
|
+
label="New Password"
|
|
78
|
+
type="password"
|
|
79
|
+
value={newPassword}
|
|
80
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
81
|
+
required
|
|
82
|
+
fullWidth
|
|
83
|
+
autoComplete="new-password"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<TextField
|
|
87
|
+
label="Confirm New Password"
|
|
88
|
+
type="password"
|
|
89
|
+
value={confirmPassword}
|
|
90
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
91
|
+
required
|
|
92
|
+
fullWidth
|
|
93
|
+
autoComplete="new-password"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<Button
|
|
97
|
+
type="submit"
|
|
98
|
+
variant="contained"
|
|
99
|
+
fullWidth
|
|
100
|
+
disabled={loading}
|
|
101
|
+
sx={{
|
|
102
|
+
py: 1.5,
|
|
103
|
+
bgcolor: 'primary.main',
|
|
104
|
+
fontWeight: 600,
|
|
105
|
+
'&:hover': { bgcolor: '#6D28D9' },
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{loading ? <CircularProgress size={24} color="inherit" /> : 'Change Password'}
|
|
109
|
+
</Button>
|
|
110
|
+
</Box>
|
|
111
|
+
</AuthLayout>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Typography, Box, Card, CardContent, Button, Container, Avatar } from '@mui/material';
|
|
2
|
+
import { Link, useNavigate } from 'react-router';
|
|
3
|
+
import { useSession, signOut } from '@/client/lib/auth-client';
|
|
4
|
+
import { AppNavbar } from '../components/AppNavbar';
|
|
5
|
+
|
|
6
|
+
export default function DashboardPage() {
|
|
7
|
+
const { data: session } = useSession();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
10
|
+
const user = session?.user;
|
|
11
|
+
|
|
12
|
+
const handleSignOut = async () => {
|
|
13
|
+
await signOut({
|
|
14
|
+
fetchOptions: {
|
|
15
|
+
onSuccess: () => navigate('/'),
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Box sx={{ minHeight: '100vh' }}>
|
|
22
|
+
<AppNavbar />
|
|
23
|
+
|
|
24
|
+
<Container maxWidth="md" sx={{ py: 6 }}>
|
|
25
|
+
<Box sx={{ animation: 'fadeInUp 0.6s ease-out' }}>
|
|
26
|
+
{/* Welcome Card */}
|
|
27
|
+
<Card
|
|
28
|
+
sx={{
|
|
29
|
+
bgcolor: 'background.paper',
|
|
30
|
+
border: '1px solid',
|
|
31
|
+
borderColor: 'rgba(124,58,237,0.15)',
|
|
32
|
+
mb: 3,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<CardContent sx={{ p: 4 }}>
|
|
36
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
|
|
37
|
+
<Avatar
|
|
38
|
+
src={user?.image || undefined}
|
|
39
|
+
sx={{
|
|
40
|
+
width: 64,
|
|
41
|
+
height: 64,
|
|
42
|
+
bgcolor: 'primary.main',
|
|
43
|
+
fontSize: '1.5rem',
|
|
44
|
+
fontWeight: 700,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{user?.name?.charAt(0)?.toUpperCase()}
|
|
48
|
+
</Avatar>
|
|
49
|
+
<Box>
|
|
50
|
+
<Typography
|
|
51
|
+
variant="h4"
|
|
52
|
+
sx={{
|
|
53
|
+
fontWeight: 700,
|
|
54
|
+
fontFamily: "'Sora', sans-serif",
|
|
55
|
+
background: 'linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%)',
|
|
56
|
+
backgroundClip: 'text',
|
|
57
|
+
WebkitBackgroundClip: 'text',
|
|
58
|
+
WebkitTextFillColor: 'transparent',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
Welcome back, {user?.name}
|
|
62
|
+
</Typography>
|
|
63
|
+
<Typography variant="body1" sx={{ color: 'text.secondary', mt: 0.5 }}>
|
|
64
|
+
{user?.email}
|
|
65
|
+
</Typography>
|
|
66
|
+
</Box>
|
|
67
|
+
</Box>
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
|
|
71
|
+
{/* Actions */}
|
|
72
|
+
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
|
73
|
+
<Button
|
|
74
|
+
component={Link}
|
|
75
|
+
to="/change-password"
|
|
76
|
+
variant="outlined"
|
|
77
|
+
sx={{
|
|
78
|
+
borderColor: 'rgba(124,58,237,0.3)',
|
|
79
|
+
color: 'text.primary',
|
|
80
|
+
'&:hover': { borderColor: 'primary.main' },
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
Change Password
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
onClick={handleSignOut}
|
|
87
|
+
variant="outlined"
|
|
88
|
+
sx={{
|
|
89
|
+
borderColor: 'rgba(124,58,237,0.3)',
|
|
90
|
+
color: 'text.primary',
|
|
91
|
+
'&:hover': { borderColor: 'primary.main' },
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
Sign Out
|
|
95
|
+
</Button>
|
|
96
|
+
</Box>
|
|
97
|
+
</Box>
|
|
98
|
+
</Container>
|
|
99
|
+
</Box>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { TextField, Button, Alert, Typography, Box, CircularProgress } from '@mui/material';
|
|
3
|
+
import { Link } from 'react-router';
|
|
4
|
+
import { authClient } from '@/client/lib/auth-client';
|
|
5
|
+
import { AuthLayout } from '../components/AuthLayout';
|
|
6
|
+
|
|
7
|
+
export default function ForgotPasswordPage() {
|
|
8
|
+
const [email, setEmail] = useState('');
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const [success, setSuccess] = useState(false);
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setError('');
|
|
16
|
+
setLoading(true);
|
|
17
|
+
|
|
18
|
+
const { error } = await authClient.requestPasswordReset({
|
|
19
|
+
email,
|
|
20
|
+
redirectTo: window.location.origin + '/reset-password',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
setLoading(false);
|
|
24
|
+
|
|
25
|
+
if (error) {
|
|
26
|
+
setError(error.message);
|
|
27
|
+
} else {
|
|
28
|
+
setSuccess(true);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<AuthLayout
|
|
34
|
+
title="Forgot your password?"
|
|
35
|
+
subtitle="Enter your email and we'll send you a reset link"
|
|
36
|
+
footer={
|
|
37
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
38
|
+
Remember your password?{' '}
|
|
39
|
+
<Typography
|
|
40
|
+
component={Link}
|
|
41
|
+
to="/login"
|
|
42
|
+
variant="body2"
|
|
43
|
+
sx={{ color: 'primary.main', textDecoration: 'none', fontWeight: 600, '&:hover': { textDecoration: 'underline' } }}
|
|
44
|
+
>
|
|
45
|
+
Back to login
|
|
46
|
+
</Typography>
|
|
47
|
+
</Typography>
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
{success ? (
|
|
51
|
+
<Alert severity="success">
|
|
52
|
+
Check your email for a password reset link.
|
|
53
|
+
</Alert>
|
|
54
|
+
) : (
|
|
55
|
+
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
56
|
+
{error && <Alert severity="error">{error}</Alert>}
|
|
57
|
+
|
|
58
|
+
<TextField
|
|
59
|
+
label="Email"
|
|
60
|
+
type="email"
|
|
61
|
+
value={email}
|
|
62
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
63
|
+
required
|
|
64
|
+
fullWidth
|
|
65
|
+
autoComplete="email"
|
|
66
|
+
autoFocus
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<Button
|
|
70
|
+
type="submit"
|
|
71
|
+
variant="contained"
|
|
72
|
+
fullWidth
|
|
73
|
+
disabled={loading}
|
|
74
|
+
sx={{
|
|
75
|
+
py: 1.5,
|
|
76
|
+
bgcolor: 'primary.main',
|
|
77
|
+
fontWeight: 600,
|
|
78
|
+
'&:hover': { bgcolor: '#6D28D9' },
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{loading ? <CircularProgress size={24} color="inherit" /> : 'Send Reset Link'}
|
|
82
|
+
</Button>
|
|
83
|
+
</Box>
|
|
84
|
+
)}
|
|
85
|
+
</AuthLayout>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Container,
|
|
3
|
+
Typography,
|
|
4
|
+
Box,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
Chip,
|
|
8
|
+
Stack,
|
|
9
|
+
Grid,
|
|
10
|
+
} from '@mui/material';
|
|
11
|
+
import { AppNavbar } from '../components/AppNavbar';
|
|
12
|
+
|
|
13
|
+
const stackItems = [
|
|
14
|
+
{
|
|
15
|
+
name: 'Cloudflare Workers',
|
|
16
|
+
description: 'Edge-first serverless runtime. Your code runs in 300+ locations worldwide.',
|
|
17
|
+
icon: '\u2601\uFE0F',
|
|
18
|
+
url: 'https://developers.cloudflare.com/workers/',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'Hono',
|
|
22
|
+
description: 'Ultrafast web framework built for the edge. Handles routing, middleware, and more.',
|
|
23
|
+
icon: '\uD83D\uDD25',
|
|
24
|
+
url: 'https://hono.dev/',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'React 19',
|
|
28
|
+
description: 'The library for building user interfaces. Server and client rendering ready.',
|
|
29
|
+
icon: '\u269B\uFE0F',
|
|
30
|
+
url: 'https://react.dev/',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'MUI Material',
|
|
34
|
+
description: 'Production-grade React component library with a comprehensive design system.',
|
|
35
|
+
icon: '\uD83C\uDFA8',
|
|
36
|
+
url: 'https://mui.com/',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Better Auth',
|
|
40
|
+
description: 'Authentication for modern apps. Email/password, OAuth, sessions, and admin.',
|
|
41
|
+
icon: '\uD83D\uDD10',
|
|
42
|
+
url: 'https://www.better-auth.com/',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Drizzle ORM',
|
|
46
|
+
description: 'Type-safe SQL ORM with zero dependencies. Migrations and schema management built-in.',
|
|
47
|
+
icon: '\uD83D\uDDC4\uFE0F',
|
|
48
|
+
url: 'https://orm.drizzle.team/',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const quickStartSteps = [
|
|
53
|
+
{ cmd: 'src/client/pages/HomePage.tsx', desc: 'Edit this file to start building your UI' },
|
|
54
|
+
{ cmd: 'src/api/index.ts', desc: 'Add your API routes here' },
|
|
55
|
+
{ cmd: 'bun run dev', desc: 'Start the development server' },
|
|
56
|
+
{ cmd: 'bun run deploy', desc: 'Deploy to Cloudflare Workers' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export default function HomePage() {
|
|
60
|
+
return (
|
|
61
|
+
<Box sx={{ minHeight: '100vh', position: 'relative', overflow: 'hidden' }}>
|
|
62
|
+
<AppNavbar />
|
|
63
|
+
|
|
64
|
+
{/* Background glow */}
|
|
65
|
+
<Box
|
|
66
|
+
sx={{
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: '-20%',
|
|
69
|
+
left: '50%',
|
|
70
|
+
transform: 'translateX(-50%)',
|
|
71
|
+
width: '600px',
|
|
72
|
+
height: '600px',
|
|
73
|
+
borderRadius: '50%',
|
|
74
|
+
background: 'radial-gradient(circle, rgba(124,58,237,0.15) 0%, transparent 70%)',
|
|
75
|
+
filter: 'blur(80px)',
|
|
76
|
+
pointerEvents: 'none',
|
|
77
|
+
animation: 'pulse 6s ease-in-out infinite',
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<Container maxWidth="md" sx={{ position: 'relative', py: 8 }}>
|
|
82
|
+
{/* Hero */}
|
|
83
|
+
<Box
|
|
84
|
+
sx={{
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
mb: 8,
|
|
87
|
+
animation: 'fadeInUp 0.8s ease-out',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Typography
|
|
91
|
+
variant="h1"
|
|
92
|
+
sx={{
|
|
93
|
+
fontSize: { xs: '2.5rem', md: '4rem' },
|
|
94
|
+
fontWeight: 800,
|
|
95
|
+
background: 'linear-gradient(135deg, #7C3AED 0%, #A78BFA 50%, #C4B5FD 100%)',
|
|
96
|
+
backgroundClip: 'text',
|
|
97
|
+
WebkitBackgroundClip: 'text',
|
|
98
|
+
WebkitTextFillColor: 'transparent',
|
|
99
|
+
mb: 2,
|
|
100
|
+
letterSpacing: '-0.02em',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
WorkerStack
|
|
104
|
+
</Typography>
|
|
105
|
+
<Typography
|
|
106
|
+
variant="h5"
|
|
107
|
+
sx={{
|
|
108
|
+
color: 'text.secondary',
|
|
109
|
+
fontWeight: 400,
|
|
110
|
+
maxWidth: '500px',
|
|
111
|
+
mx: 'auto',
|
|
112
|
+
lineHeight: 1.6,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
Full-stack Cloudflare Workers starter with everything you need to ship fast.
|
|
116
|
+
</Typography>
|
|
117
|
+
</Box>
|
|
118
|
+
|
|
119
|
+
{/* Stack Grid */}
|
|
120
|
+
<Box sx={{ mb: 8 }}>
|
|
121
|
+
<Grid container spacing={2}>
|
|
122
|
+
{stackItems.map((item) => (
|
|
123
|
+
<Grid size={{ xs: 12, sm: 6 }} key={item.name}>
|
|
124
|
+
<Card
|
|
125
|
+
component="a"
|
|
126
|
+
href={item.url}
|
|
127
|
+
target="_blank"
|
|
128
|
+
rel="noopener noreferrer"
|
|
129
|
+
sx={{
|
|
130
|
+
height: '100%',
|
|
131
|
+
display: 'flex',
|
|
132
|
+
bgcolor: 'background.paper',
|
|
133
|
+
border: '1px solid',
|
|
134
|
+
borderColor: 'rgba(124,58,237,0.15)',
|
|
135
|
+
textDecoration: 'none',
|
|
136
|
+
transition: 'all 0.2s ease',
|
|
137
|
+
'&:hover': {
|
|
138
|
+
borderColor: 'primary.main',
|
|
139
|
+
transform: 'translateY(-2px)',
|
|
140
|
+
boxShadow: '0 8px 30px rgba(124,58,237,0.15)',
|
|
141
|
+
},
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<CardContent sx={{ p: 3 }}>
|
|
145
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1 }}>
|
|
146
|
+
<Typography sx={{ fontSize: '1.5rem' }}>{item.icon}</Typography>
|
|
147
|
+
<Typography variant="h6" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
|
148
|
+
{item.name}
|
|
149
|
+
</Typography>
|
|
150
|
+
</Box>
|
|
151
|
+
<Typography variant="body2" sx={{ color: 'text.secondary', lineHeight: 1.6 }}>
|
|
152
|
+
{item.description}
|
|
153
|
+
</Typography>
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
</Grid>
|
|
157
|
+
))}
|
|
158
|
+
</Grid>
|
|
159
|
+
</Box>
|
|
160
|
+
|
|
161
|
+
{/* Quick Start */}
|
|
162
|
+
<Box
|
|
163
|
+
sx={{
|
|
164
|
+
mb: 8,
|
|
165
|
+
animation: 'fadeInUp 0.8s ease-out 0.2s both',
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<Typography
|
|
169
|
+
variant="h5"
|
|
170
|
+
sx={{
|
|
171
|
+
fontWeight: 700,
|
|
172
|
+
mb: 3,
|
|
173
|
+
textAlign: 'center',
|
|
174
|
+
fontFamily: "'Sora', sans-serif",
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
Get started
|
|
178
|
+
</Typography>
|
|
179
|
+
<Box
|
|
180
|
+
sx={{
|
|
181
|
+
bgcolor: (theme) =>
|
|
182
|
+
theme.palette.mode === 'dark' ? '#0D0D14' : '#F1F3F5',
|
|
183
|
+
border: '1px solid',
|
|
184
|
+
borderColor: 'rgba(124,58,237,0.2)',
|
|
185
|
+
borderRadius: 2,
|
|
186
|
+
overflow: 'hidden',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{/* Terminal header */}
|
|
190
|
+
<Box
|
|
191
|
+
sx={{
|
|
192
|
+
px: 2,
|
|
193
|
+
py: 1.5,
|
|
194
|
+
borderBottom: '1px solid',
|
|
195
|
+
borderColor: (theme) =>
|
|
196
|
+
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)',
|
|
197
|
+
display: 'flex',
|
|
198
|
+
gap: 1,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#FF5F57' }} />
|
|
202
|
+
<Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#FFBD2E' }} />
|
|
203
|
+
<Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#28C840' }} />
|
|
204
|
+
</Box>
|
|
205
|
+
{/* Terminal content */}
|
|
206
|
+
<Box sx={{ p: 3 }}>
|
|
207
|
+
{quickStartSteps.map((step, i) => (
|
|
208
|
+
<Box key={i} sx={{ mb: i < quickStartSteps.length - 1 ? 2 : 0 }}>
|
|
209
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
210
|
+
<Typography
|
|
211
|
+
component="span"
|
|
212
|
+
sx={{ color: '#7C3AED', fontFamily: 'monospace', fontWeight: 700 }}
|
|
213
|
+
>
|
|
214
|
+
{'\u276F'}
|
|
215
|
+
</Typography>
|
|
216
|
+
<Typography
|
|
217
|
+
component="span"
|
|
218
|
+
sx={{ color: '#C4B5FD', fontFamily: 'monospace', fontSize: '0.95rem' }}
|
|
219
|
+
>
|
|
220
|
+
{step.cmd}
|
|
221
|
+
</Typography>
|
|
222
|
+
</Box>
|
|
223
|
+
<Typography
|
|
224
|
+
sx={{
|
|
225
|
+
color: 'text.secondary',
|
|
226
|
+
fontSize: '0.85rem',
|
|
227
|
+
pl: 3,
|
|
228
|
+
mt: 0.3,
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{step.desc}
|
|
232
|
+
</Typography>
|
|
233
|
+
</Box>
|
|
234
|
+
))}
|
|
235
|
+
</Box>
|
|
236
|
+
</Box>
|
|
237
|
+
</Box>
|
|
238
|
+
|
|
239
|
+
{/* Doc Links */}
|
|
240
|
+
<Box sx={{ textAlign: 'center', mb: 6 }}>
|
|
241
|
+
<Stack
|
|
242
|
+
direction="row"
|
|
243
|
+
spacing={1}
|
|
244
|
+
flexWrap="wrap"
|
|
245
|
+
justifyContent="center"
|
|
246
|
+
useFlexGap
|
|
247
|
+
sx={{ gap: 1 }}
|
|
248
|
+
>
|
|
249
|
+
{stackItems.map((item) => (
|
|
250
|
+
<Chip
|
|
251
|
+
key={item.name}
|
|
252
|
+
label={item.name}
|
|
253
|
+
component="a"
|
|
254
|
+
href={item.url}
|
|
255
|
+
target="_blank"
|
|
256
|
+
rel="noopener noreferrer"
|
|
257
|
+
clickable
|
|
258
|
+
sx={{
|
|
259
|
+
bgcolor: 'rgba(124,58,237,0.1)',
|
|
260
|
+
border: '1px solid rgba(124,58,237,0.2)',
|
|
261
|
+
color: '#C4B5FD',
|
|
262
|
+
fontWeight: 500,
|
|
263
|
+
'&:hover': {
|
|
264
|
+
bgcolor: 'rgba(124,58,237,0.2)',
|
|
265
|
+
},
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
269
|
+
</Stack>
|
|
270
|
+
</Box>
|
|
271
|
+
|
|
272
|
+
{/* Footer */}
|
|
273
|
+
<Box sx={{ textAlign: 'center', pb: 4 }}>
|
|
274
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
275
|
+
Built with{' '}
|
|
276
|
+
<Typography
|
|
277
|
+
component="span"
|
|
278
|
+
variant="body2"
|
|
279
|
+
sx={{
|
|
280
|
+
background: 'linear-gradient(135deg, #7C3AED, #A78BFA)',
|
|
281
|
+
backgroundClip: 'text',
|
|
282
|
+
WebkitBackgroundClip: 'text',
|
|
283
|
+
WebkitTextFillColor: 'transparent',
|
|
284
|
+
fontWeight: 600,
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
WorkerStack
|
|
288
|
+
</Typography>
|
|
289
|
+
</Typography>
|
|
290
|
+
</Box>
|
|
291
|
+
</Container>
|
|
292
|
+
</Box>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { TextField, Button, Alert, Typography, Box, CircularProgress } from '@mui/material';
|
|
3
|
+
import { Link, useNavigate, useLocation, Navigate } from 'react-router';
|
|
4
|
+
import { signIn, useSession } from '@/client/lib/auth-client';
|
|
5
|
+
import { AuthLayout } from '../components/AuthLayout';
|
|
6
|
+
|
|
7
|
+
export default function LoginPage() {
|
|
8
|
+
const { data: session, isPending: sessionLoading } = useSession();
|
|
9
|
+
const navigate = useNavigate();
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
const from = (location.state as { from?: string })?.from || '/dashboard';
|
|
12
|
+
|
|
13
|
+
const [email, setEmail] = useState('');
|
|
14
|
+
const [password, setPassword] = useState('');
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
if (sessionLoading) return null;
|
|
19
|
+
if (session) return <Navigate to="/dashboard" replace />;
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setError('');
|
|
24
|
+
setLoading(true);
|
|
25
|
+
|
|
26
|
+
await signIn.email({
|
|
27
|
+
email,
|
|
28
|
+
password,
|
|
29
|
+
}, {
|
|
30
|
+
onSuccess: () => navigate(from, { replace: true }),
|
|
31
|
+
onError: (ctx) => {
|
|
32
|
+
setError(ctx.error.message);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<AuthLayout
|
|
40
|
+
title="Welcome back"
|
|
41
|
+
subtitle="Sign in to your account"
|
|
42
|
+
footer={
|
|
43
|
+
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
44
|
+
Don't have an account?{' '}
|
|
45
|
+
<Typography
|
|
46
|
+
component={Link}
|
|
47
|
+
to="/register"
|
|
48
|
+
variant="body2"
|
|
49
|
+
sx={{ color: 'primary.main', textDecoration: 'none', fontWeight: 600, '&:hover': { textDecoration: 'underline' } }}
|
|
50
|
+
>
|
|
51
|
+
Register
|
|
52
|
+
</Typography>
|
|
53
|
+
</Typography>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
57
|
+
{error && <Alert severity="error">{error}</Alert>}
|
|
58
|
+
|
|
59
|
+
<TextField
|
|
60
|
+
label="Email"
|
|
61
|
+
type="email"
|
|
62
|
+
value={email}
|
|
63
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
64
|
+
required
|
|
65
|
+
fullWidth
|
|
66
|
+
autoComplete="email"
|
|
67
|
+
autoFocus
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<TextField
|
|
71
|
+
label="Password"
|
|
72
|
+
type="password"
|
|
73
|
+
value={password}
|
|
74
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
75
|
+
required
|
|
76
|
+
fullWidth
|
|
77
|
+
autoComplete="current-password"
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<Box sx={{ textAlign: 'right', mt: -1 }}>
|
|
81
|
+
<Typography
|
|
82
|
+
component={Link}
|
|
83
|
+
to="/forgot-password"
|
|
84
|
+
variant="body2"
|
|
85
|
+
sx={{ color: 'primary.main', textDecoration: 'none', '&:hover': { textDecoration: 'underline' } }}
|
|
86
|
+
>
|
|
87
|
+
Forgot password?
|
|
88
|
+
</Typography>
|
|
89
|
+
</Box>
|
|
90
|
+
|
|
91
|
+
<Button
|
|
92
|
+
type="submit"
|
|
93
|
+
variant="contained"
|
|
94
|
+
fullWidth
|
|
95
|
+
disabled={loading}
|
|
96
|
+
sx={{
|
|
97
|
+
py: 1.5,
|
|
98
|
+
bgcolor: 'primary.main',
|
|
99
|
+
fontWeight: 600,
|
|
100
|
+
'&:hover': { bgcolor: '#6D28D9' },
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{loading ? <CircularProgress size={24} color="inherit" /> : 'Sign In'}
|
|
104
|
+
</Button>
|
|
105
|
+
</Box>
|
|
106
|
+
</AuthLayout>
|
|
107
|
+
);
|
|
108
|
+
}
|