create-workerstack 0.1.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-workerstack",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Scaffold a full-stack Cloudflare Workers app with Hono, React, MUI, Better Auth, and Drizzle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
25
25
  "postgres": "^3.4.8",
26
26
  "react": "^19.2.4",
27
27
  "react-dom": "^19.2.4",
28
+ "react-router": "^7.6.2",
28
29
  "resend": "^6.9.4"
29
30
  },
30
31
  "devDependencies": {
@@ -1,314 +1,32 @@
1
- import {
2
- ThemeProvider,
3
- createTheme,
4
- CssBaseline,
5
- Container,
6
- Typography,
7
- Box,
8
- Card,
9
- CardContent,
10
- Chip,
11
- Stack,
12
- Grid,
13
- } from '@mui/material';
14
-
15
- const darkTheme = createTheme({
16
- palette: {
17
- mode: 'dark',
18
- background: { default: '#0A0A0F', paper: '#12121A' },
19
- text: { primary: '#F1F5F9', secondary: '#94A3B8' },
20
- primary: { main: '#7C3AED' },
21
- },
22
- typography: {
23
- fontFamily: "'Plus Jakarta Sans', sans-serif",
24
- h1: { fontFamily: "'Sora', sans-serif" },
25
- h2: { fontFamily: "'Sora', sans-serif" },
26
- },
27
- shape: { borderRadius: 12 },
28
- });
29
-
30
- const stackItems = [
31
- {
32
- name: 'Cloudflare Workers',
33
- description: 'Edge-first serverless runtime. Your code runs in 300+ locations worldwide.',
34
- icon: '\u2601\uFE0F',
35
- url: 'https://developers.cloudflare.com/workers/',
36
- },
37
- {
38
- name: 'Hono',
39
- description: 'Ultrafast web framework built for the edge. Handles routing, middleware, and more.',
40
- icon: '\uD83D\uDD25',
41
- url: 'https://hono.dev/',
42
- },
43
- {
44
- name: 'React 19',
45
- description: 'The library for building user interfaces. Server and client rendering ready.',
46
- icon: '\u269B\uFE0F',
47
- url: 'https://react.dev/',
48
- },
49
- {
50
- name: 'MUI Material',
51
- description: 'Production-grade React component library with a comprehensive design system.',
52
- icon: '\uD83C\uDFA8',
53
- url: 'https://mui.com/',
54
- },
55
- {
56
- name: 'Better Auth',
57
- description: 'Authentication for modern apps. Email/password, OAuth, sessions, and admin.',
58
- icon: '\uD83D\uDD10',
59
- url: 'https://www.better-auth.com/',
60
- },
61
- {
62
- name: 'Drizzle ORM',
63
- description: 'Type-safe SQL ORM with zero dependencies. Migrations and schema management built-in.',
64
- icon: '\uD83D\uDDC4\uFE0F',
65
- url: 'https://orm.drizzle.team/',
66
- },
67
- ];
68
-
69
- const quickStartSteps = [
70
- { cmd: 'src/client/App.tsx', desc: 'Edit this file to start building your UI' },
71
- { cmd: 'src/api/index.ts', desc: 'Add your API routes here' },
72
- { cmd: 'bun run dev', desc: 'Start the development server' },
73
- { cmd: 'bun run deploy', desc: 'Deploy to Cloudflare Workers' },
74
- ];
1
+ import { BrowserRouter, Routes, Route } from 'react-router';
2
+ import { ThemeProvider, CssBaseline } from '@mui/material';
3
+ import theme from './theme';
4
+ import { ProtectedRoute } from './components/ProtectedRoute';
5
+ import HomePage from './pages/HomePage';
6
+ import LoginPage from './pages/LoginPage';
7
+ import RegisterPage from './pages/RegisterPage';
8
+ import ForgotPasswordPage from './pages/ForgotPasswordPage';
9
+ import ResetPasswordPage from './pages/ResetPasswordPage';
10
+ import ChangePasswordPage from './pages/ChangePasswordPage';
11
+ import DashboardPage from './pages/DashboardPage';
12
+ import NotFoundPage from './pages/NotFoundPage';
75
13
 
76
14
  function App() {
77
15
  return (
78
- <ThemeProvider theme={darkTheme}>
79
- <CssBaseline />
80
- <Box
81
- sx={{
82
- minHeight: '100vh',
83
- position: 'relative',
84
- overflow: 'hidden',
85
- }}
86
- >
87
- {/* Background glow */}
88
- <Box
89
- sx={{
90
- position: 'absolute',
91
- top: '-20%',
92
- left: '50%',
93
- transform: 'translateX(-50%)',
94
- width: '600px',
95
- height: '600px',
96
- borderRadius: '50%',
97
- background: 'radial-gradient(circle, rgba(124,58,237,0.15) 0%, transparent 70%)',
98
- filter: 'blur(80px)',
99
- pointerEvents: 'none',
100
- animation: 'pulse 6s ease-in-out infinite',
101
- }}
102
- />
103
-
104
- <Container maxWidth="md" sx={{ position: 'relative', py: 8 }}>
105
- {/* Hero */}
106
- <Box
107
- sx={{
108
- textAlign: 'center',
109
- mb: 8,
110
- animation: 'fadeInUp 0.8s ease-out',
111
- }}
112
- >
113
- <Typography
114
- variant="h1"
115
- sx={{
116
- fontSize: { xs: '2.5rem', md: '4rem' },
117
- fontWeight: 800,
118
- background: 'linear-gradient(135deg, #7C3AED 0%, #A78BFA 50%, #C4B5FD 100%)',
119
- backgroundClip: 'text',
120
- WebkitBackgroundClip: 'text',
121
- WebkitTextFillColor: 'transparent',
122
- mb: 2,
123
- letterSpacing: '-0.02em',
124
- }}
125
- >
126
- WorkerStack
127
- </Typography>
128
- <Typography
129
- variant="h5"
130
- sx={{
131
- color: 'text.secondary',
132
- fontWeight: 400,
133
- maxWidth: '500px',
134
- mx: 'auto',
135
- lineHeight: 1.6,
136
- }}
137
- >
138
- Full-stack Cloudflare Workers starter with everything you need to ship fast.
139
- </Typography>
140
- </Box>
141
-
142
- {/* Stack Grid */}
143
- <Box sx={{ mb: 8 }}>
144
- <Grid container spacing={2}>
145
- {stackItems.map((item) => (
146
- <Grid size={{ xs: 12, sm: 6 }} key={item.name}>
147
- <Card
148
- component="a"
149
- href={item.url}
150
- target="_blank"
151
- rel="noopener noreferrer"
152
- sx={{
153
- height: '100%',
154
- display: 'flex',
155
- bgcolor: 'background.paper',
156
- border: '1px solid',
157
- borderColor: 'rgba(124,58,237,0.15)',
158
- textDecoration: 'none',
159
- transition: 'all 0.2s ease',
160
- '&:hover': {
161
- borderColor: 'primary.main',
162
- transform: 'translateY(-2px)',
163
- boxShadow: '0 8px 30px rgba(124,58,237,0.15)',
164
- },
165
- }}
166
- >
167
- <CardContent sx={{ p: 3 }}>
168
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1 }}>
169
- <Typography sx={{ fontSize: '1.5rem' }}>{item.icon}</Typography>
170
- <Typography variant="h6" sx={{ fontWeight: 700, color: 'text.primary' }}>
171
- {item.name}
172
- </Typography>
173
- </Box>
174
- <Typography variant="body2" sx={{ color: 'text.secondary', lineHeight: 1.6 }}>
175
- {item.description}
176
- </Typography>
177
- </CardContent>
178
- </Card>
179
- </Grid>
180
- ))}
181
- </Grid>
182
- </Box>
183
-
184
- {/* Quick Start */}
185
- <Box
186
- sx={{
187
- mb: 8,
188
- animation: 'fadeInUp 0.8s ease-out 0.2s both',
189
- }}
190
- >
191
- <Typography
192
- variant="h5"
193
- sx={{
194
- fontWeight: 700,
195
- mb: 3,
196
- textAlign: 'center',
197
- fontFamily: "'Sora', sans-serif",
198
- }}
199
- >
200
- Get started
201
- </Typography>
202
- <Box
203
- sx={{
204
- bgcolor: '#0D0D14',
205
- border: '1px solid rgba(124,58,237,0.2)',
206
- borderRadius: 2,
207
- overflow: 'hidden',
208
- }}
209
- >
210
- {/* Terminal header */}
211
- <Box
212
- sx={{
213
- px: 2,
214
- py: 1.5,
215
- borderBottom: '1px solid rgba(255,255,255,0.06)',
216
- display: 'flex',
217
- gap: 1,
218
- }}
219
- >
220
- <Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#FF5F57' }} />
221
- <Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#FFBD2E' }} />
222
- <Box sx={{ width: 12, height: 12, borderRadius: '50%', bgcolor: '#28C840' }} />
223
- </Box>
224
- {/* Terminal content */}
225
- <Box sx={{ p: 3 }}>
226
- {quickStartSteps.map((step, i) => (
227
- <Box key={i} sx={{ mb: i < quickStartSteps.length - 1 ? 2 : 0 }}>
228
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
229
- <Typography
230
- component="span"
231
- sx={{ color: '#7C3AED', fontFamily: 'monospace', fontWeight: 700 }}
232
- >
233
- {'\u276F'}
234
- </Typography>
235
- <Typography
236
- component="span"
237
- sx={{ color: '#C4B5FD', fontFamily: 'monospace', fontSize: '0.95rem' }}
238
- >
239
- {step.cmd}
240
- </Typography>
241
- </Box>
242
- <Typography
243
- sx={{
244
- color: 'text.secondary',
245
- fontSize: '0.85rem',
246
- pl: 3,
247
- mt: 0.3,
248
- }}
249
- >
250
- {step.desc}
251
- </Typography>
252
- </Box>
253
- ))}
254
- </Box>
255
- </Box>
256
- </Box>
257
-
258
- {/* Doc Links */}
259
- <Box sx={{ textAlign: 'center', mb: 6 }}>
260
- <Stack
261
- direction="row"
262
- spacing={1}
263
- flexWrap="wrap"
264
- justifyContent="center"
265
- useFlexGap
266
- sx={{ gap: 1 }}
267
- >
268
- {stackItems.map((item) => (
269
- <Chip
270
- key={item.name}
271
- label={item.name}
272
- component="a"
273
- href={item.url}
274
- target="_blank"
275
- rel="noopener noreferrer"
276
- clickable
277
- sx={{
278
- bgcolor: 'rgba(124,58,237,0.1)',
279
- border: '1px solid rgba(124,58,237,0.2)',
280
- color: '#C4B5FD',
281
- fontWeight: 500,
282
- '&:hover': {
283
- bgcolor: 'rgba(124,58,237,0.2)',
284
- },
285
- }}
286
- />
287
- ))}
288
- </Stack>
289
- </Box>
290
-
291
- {/* Footer */}
292
- <Box sx={{ textAlign: 'center', pb: 4 }}>
293
- <Typography variant="body2" sx={{ color: 'text.secondary' }}>
294
- Built with{' '}
295
- <Typography
296
- component="span"
297
- variant="body2"
298
- sx={{
299
- background: 'linear-gradient(135deg, #7C3AED, #A78BFA)',
300
- backgroundClip: 'text',
301
- WebkitBackgroundClip: 'text',
302
- WebkitTextFillColor: 'transparent',
303
- fontWeight: 600,
304
- }}
305
- >
306
- WorkerStack
307
- </Typography>
308
- </Typography>
309
- </Box>
310
- </Container>
311
- </Box>
16
+ <ThemeProvider theme={theme} noSsr>
17
+ <CssBaseline enableColorScheme />
18
+ <BrowserRouter>
19
+ <Routes>
20
+ <Route path="/" element={<HomePage />} />
21
+ <Route path="/login" element={<LoginPage />} />
22
+ <Route path="/register" element={<RegisterPage />} />
23
+ <Route path="/forgot-password" element={<ForgotPasswordPage />} />
24
+ <Route path="/reset-password" element={<ResetPasswordPage />} />
25
+ <Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
26
+ <Route path="/change-password" element={<ProtectedRoute><ChangePasswordPage /></ProtectedRoute>} />
27
+ <Route path="*" element={<NotFoundPage />} />
28
+ </Routes>
29
+ </BrowserRouter>
312
30
  </ThemeProvider>
313
31
  );
314
32
  }
@@ -0,0 +1,100 @@
1
+ import { AppBar, Toolbar, Typography, Button, Box } from '@mui/material';
2
+ import { Link, useNavigate } from 'react-router';
3
+ import { useSession, signOut } from '@/client/lib/auth-client';
4
+ import { ThemeToggle } from './ThemeToggle';
5
+
6
+ export function AppNavbar() {
7
+ const { data: session } = useSession();
8
+ const navigate = useNavigate();
9
+
10
+ const handleSignOut = async () => {
11
+ await signOut({
12
+ fetchOptions: {
13
+ onSuccess: () => navigate('/'),
14
+ },
15
+ });
16
+ };
17
+
18
+ return (
19
+ <AppBar
20
+ position="static"
21
+ elevation={0}
22
+ sx={{
23
+ bgcolor: 'transparent',
24
+ borderBottom: '1px solid',
25
+ borderColor: 'rgba(124,58,237,0.1)',
26
+ }}
27
+ >
28
+ <Toolbar sx={{ justifyContent: 'space-between' }}>
29
+ <Typography
30
+ component={Link}
31
+ to="/"
32
+ sx={{
33
+ fontWeight: 800,
34
+ fontFamily: "'Sora', sans-serif",
35
+ fontSize: '1.25rem',
36
+ background: 'linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%)',
37
+ backgroundClip: 'text',
38
+ WebkitBackgroundClip: 'text',
39
+ WebkitTextFillColor: 'transparent',
40
+ textDecoration: 'none',
41
+ }}
42
+ >
43
+ WorkerStack
44
+ </Typography>
45
+
46
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
47
+ <ThemeToggle />
48
+
49
+ {session ? (
50
+ <>
51
+ <Button
52
+ component={Link}
53
+ to="/dashboard"
54
+ size="small"
55
+ sx={{ color: 'text.primary' }}
56
+ >
57
+ Dashboard
58
+ </Button>
59
+ <Button
60
+ onClick={handleSignOut}
61
+ size="small"
62
+ variant="outlined"
63
+ sx={{
64
+ borderColor: 'rgba(124,58,237,0.3)',
65
+ color: 'text.primary',
66
+ '&:hover': { borderColor: 'primary.main' },
67
+ }}
68
+ >
69
+ Sign Out
70
+ </Button>
71
+ </>
72
+ ) : (
73
+ <>
74
+ <Button
75
+ component={Link}
76
+ to="/login"
77
+ size="small"
78
+ sx={{ color: 'text.primary' }}
79
+ >
80
+ Login
81
+ </Button>
82
+ <Button
83
+ component={Link}
84
+ to="/register"
85
+ size="small"
86
+ variant="contained"
87
+ sx={{
88
+ bgcolor: 'primary.main',
89
+ '&:hover': { bgcolor: '#6D28D9' },
90
+ }}
91
+ >
92
+ Register
93
+ </Button>
94
+ </>
95
+ )}
96
+ </Box>
97
+ </Toolbar>
98
+ </AppBar>
99
+ );
100
+ }
@@ -0,0 +1,111 @@
1
+ import { Box, Card, CardContent, Typography } from '@mui/material';
2
+ import { Link } from 'react-router';
3
+ import { ThemeToggle } from './ThemeToggle';
4
+
5
+ interface AuthLayoutProps {
6
+ title: string;
7
+ subtitle?: string;
8
+ children: React.ReactNode;
9
+ footer?: React.ReactNode;
10
+ }
11
+
12
+ export function AuthLayout({ title, subtitle, children, footer }: AuthLayoutProps) {
13
+ return (
14
+ <Box
15
+ sx={{
16
+ minHeight: '100vh',
17
+ display: 'flex',
18
+ flexDirection: 'column',
19
+ alignItems: 'center',
20
+ justifyContent: 'center',
21
+ position: 'relative',
22
+ overflow: 'hidden',
23
+ px: 2,
24
+ }}
25
+ >
26
+ {/* Background glow */}
27
+ <Box
28
+ sx={{
29
+ position: 'absolute',
30
+ top: '-20%',
31
+ left: '50%',
32
+ transform: 'translateX(-50%)',
33
+ width: '600px',
34
+ height: '600px',
35
+ borderRadius: '50%',
36
+ background: 'radial-gradient(circle, rgba(124,58,237,0.15) 0%, transparent 70%)',
37
+ filter: 'blur(80px)',
38
+ pointerEvents: 'none',
39
+ animation: 'pulse 6s ease-in-out infinite',
40
+ }}
41
+ />
42
+
43
+ {/* Theme toggle */}
44
+ <Box sx={{ position: 'absolute', top: 16, right: 16 }}>
45
+ <ThemeToggle />
46
+ </Box>
47
+
48
+ {/* Brand */}
49
+ <Typography
50
+ component={Link}
51
+ to="/"
52
+ sx={{
53
+ fontSize: '1.5rem',
54
+ fontWeight: 800,
55
+ fontFamily: "'Sora', sans-serif",
56
+ background: 'linear-gradient(135deg, #7C3AED 0%, #A78BFA 50%, #C4B5FD 100%)',
57
+ backgroundClip: 'text',
58
+ WebkitBackgroundClip: 'text',
59
+ WebkitTextFillColor: 'transparent',
60
+ textDecoration: 'none',
61
+ mb: 4,
62
+ }}
63
+ >
64
+ WorkerStack
65
+ </Typography>
66
+
67
+ <Card
68
+ sx={{
69
+ width: '100%',
70
+ maxWidth: 440,
71
+ bgcolor: 'background.paper',
72
+ border: '1px solid',
73
+ borderColor: 'rgba(124,58,237,0.15)',
74
+ position: 'relative',
75
+ animation: 'fadeInUp 0.6s ease-out',
76
+ }}
77
+ >
78
+ <CardContent sx={{ p: 4 }}>
79
+ <Typography
80
+ variant="h5"
81
+ sx={{
82
+ fontWeight: 700,
83
+ fontFamily: "'Sora', sans-serif",
84
+ mb: subtitle ? 1 : 3,
85
+ textAlign: 'center',
86
+ }}
87
+ >
88
+ {title}
89
+ </Typography>
90
+
91
+ {subtitle && (
92
+ <Typography
93
+ variant="body2"
94
+ sx={{ color: 'text.secondary', mb: 3, textAlign: 'center' }}
95
+ >
96
+ {subtitle}
97
+ </Typography>
98
+ )}
99
+
100
+ {children}
101
+ </CardContent>
102
+ </Card>
103
+
104
+ {footer && (
105
+ <Box sx={{ mt: 2, textAlign: 'center', animation: 'fadeInUp 0.6s ease-out 0.1s both' }}>
106
+ {footer}
107
+ </Box>
108
+ )}
109
+ </Box>
110
+ );
111
+ }
@@ -0,0 +1,22 @@
1
+ import { useSession } from '@/client/lib/auth-client';
2
+ import { Navigate, useLocation } from 'react-router';
3
+ import { CircularProgress, Box } from '@mui/material';
4
+
5
+ export function ProtectedRoute({ children }: { children: React.ReactNode }) {
6
+ const { data: session, isPending } = useSession();
7
+ const location = useLocation();
8
+
9
+ if (isPending) {
10
+ return (
11
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
12
+ <CircularProgress sx={{ color: 'primary.main' }} />
13
+ </Box>
14
+ );
15
+ }
16
+
17
+ if (!session) {
18
+ return <Navigate to="/login" state={{ from: location.pathname }} replace />;
19
+ }
20
+
21
+ return <>{children}</>;
22
+ }
@@ -0,0 +1,30 @@
1
+ import { useColorScheme } from '@mui/material/styles';
2
+ import { IconButton, Tooltip } from '@mui/material';
3
+
4
+ export function ThemeToggle() {
5
+ const { mode, setMode } = useColorScheme();
6
+
7
+ if (!mode) return null;
8
+
9
+ const isDark = mode === 'dark';
10
+
11
+ return (
12
+ <Tooltip title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}>
13
+ <IconButton
14
+ onClick={() => setMode(isDark ? 'light' : 'dark')}
15
+ sx={{ color: 'text.primary' }}
16
+ >
17
+ {isDark ? (
18
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
19
+ <circle cx="12" cy="12" r="5" />
20
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
21
+ </svg>
22
+ ) : (
23
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
24
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
25
+ </svg>
26
+ )}
27
+ </IconButton>
28
+ </Tooltip>
29
+ );
30
+ }
@@ -0,0 +1,17 @@
1
+ import { createAuthClient } from "better-auth/react"
2
+ import { adminClient } from "better-auth/client/plugins"
3
+
4
+ export const authClient = createAuthClient({
5
+ plugins: [
6
+ adminClient(),
7
+ ],
8
+ })
9
+
10
+ export const {
11
+ useSession,
12
+ signIn,
13
+ signUp,
14
+ signOut,
15
+ } = authClient
16
+
17
+ export { authClient }