create-ej-app 8.0.0 → 11.0.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/package.json +2 -2
- package/template/base/.dockerignore +7 -0
- package/template/base/package.json +16 -14
- package/template/base/src/app/(app)/layout.tsx +1 -1
- package/template/base/src/app/(auth)/login/email-login-form.tsx +9 -1
- package/template/base/src/components/site-footer.tsx +2 -2
- package/template/base/src/lib/auth.ts +22 -3
- package/template/base/src/lib/safe-action-client.ts +75 -0
- package/template/base/src/lib/toast-message.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ej-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "Bootstrap a next.js app with my preferred setup",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"scripts": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"enquirer": "^2.4.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^25.0.
|
|
38
|
+
"@types/node": "^25.0.3",
|
|
39
39
|
"typescript": "^5.9.3"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "next dev",
|
|
8
|
-
"build": "next build",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"build": "bun --bun next build",
|
|
9
|
+
"build:docker": "docker build -t my-app .",
|
|
10
|
+
"analyze": "ANALYZE=true bun --bun next build --webpack",
|
|
11
|
+
"start": "bun --bun next start",
|
|
11
12
|
"lint": "eslint",
|
|
12
13
|
"format": "prettier --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
|
|
13
14
|
"db:push": "drizzle-kit push",
|
|
@@ -16,19 +17,20 @@
|
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"@hookform/resolvers": "5.2.2",
|
|
19
|
-
"@next/third-parties": "16.0
|
|
20
|
+
"@next/third-parties": "16.1.0",
|
|
20
21
|
"@paralleldrive/cuid2": "3.0.4",
|
|
21
22
|
"@radix-ui/react-label": "2.1.8",
|
|
22
23
|
"@radix-ui/react-separator": "1.1.8",
|
|
23
24
|
"@radix-ui/react-slot": "1.2.4",
|
|
24
|
-
"@t3-oss/env-nextjs": "0.13.
|
|
25
|
+
"@t3-oss/env-nextjs": "0.13.10",
|
|
25
26
|
"@tanstack/react-query": "5.90.12",
|
|
26
|
-
"better-auth": "1.4.
|
|
27
|
+
"better-auth": "1.4.7",
|
|
27
28
|
"class-variance-authority": "0.7.1",
|
|
28
29
|
"clsx": "2.1.1",
|
|
29
30
|
"drizzle-orm": "0.45.1",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
31
|
+
"fakefilter": "0.1.1401",
|
|
32
|
+
"lucide-react": "0.562.0",
|
|
33
|
+
"next": "16.1.0",
|
|
32
34
|
"next-safe-action": "8.0.11",
|
|
33
35
|
"next-themes": "0.4.6",
|
|
34
36
|
"nextjs-toploader": "3.9.17",
|
|
@@ -36,24 +38,24 @@
|
|
|
36
38
|
"postgres": "3.4.7",
|
|
37
39
|
"react": "19.2.3",
|
|
38
40
|
"react-dom": "19.2.3",
|
|
39
|
-
"react-hook-form": "7.
|
|
41
|
+
"react-hook-form": "7.69.0",
|
|
40
42
|
"sonner": "2.0.7",
|
|
41
43
|
"tailwind-merge": "3.4.0",
|
|
42
44
|
"tw-animate-css": "1.4.0",
|
|
43
|
-
"workflow": "4.0.1-beta.
|
|
44
|
-
"zod": "4.1
|
|
45
|
+
"workflow": "4.0.1-beta.35",
|
|
46
|
+
"zod": "4.2.1"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
|
-
"@next/bundle-analyzer": "16.0
|
|
49
|
+
"@next/bundle-analyzer": "16.1.0",
|
|
48
50
|
"@tailwindcss/postcss": "4.1.18",
|
|
49
|
-
"@types/bun": "1.3.
|
|
51
|
+
"@types/bun": "1.3.5",
|
|
50
52
|
"@types/nodemailer": "7.0.4",
|
|
51
53
|
"@types/react": "19.2.7",
|
|
52
54
|
"@types/react-dom": "19.2.3",
|
|
53
55
|
"babel-plugin-react-compiler": "1.0.0",
|
|
54
56
|
"drizzle-kit": "0.31.8",
|
|
55
57
|
"eslint": "9.39.2",
|
|
56
|
-
"eslint-config-next": "16.0
|
|
58
|
+
"eslint-config-next": "16.1.0",
|
|
57
59
|
"prettier": "3.7.4",
|
|
58
60
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
59
61
|
"prettier-plugin-tailwindcss": "0.7.2",
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '~/components/ui/form'
|
|
17
17
|
import { Input } from '~/components/ui/input'
|
|
18
18
|
import { signIn } from '~/lib/auth-client'
|
|
19
|
-
import { toastSuccessMessage } from '~/lib/toast-message'
|
|
19
|
+
import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
|
|
20
20
|
|
|
21
21
|
const emailLoginFormSchema = z.object({
|
|
22
22
|
email: z.email()
|
|
@@ -49,6 +49,14 @@ export const EmailLoginForm = () => {
|
|
|
49
49
|
toastSuccessMessage('Magic link sent! Check your email.')
|
|
50
50
|
} catch (err) {
|
|
51
51
|
console.error('Error sending magic link:', err)
|
|
52
|
+
if (
|
|
53
|
+
typeof err === 'object' &&
|
|
54
|
+
err &&
|
|
55
|
+
'message' in err &&
|
|
56
|
+
typeof err.message === 'string'
|
|
57
|
+
)
|
|
58
|
+
toastErrorMessage(err.message)
|
|
59
|
+
else toastErrorMessage('Something went wrong. Please try again.')
|
|
52
60
|
} finally {
|
|
53
61
|
setIsLoading(false)
|
|
54
62
|
}
|
|
@@ -3,8 +3,8 @@ import { siteConfig } from '~/lib/siteConfig'
|
|
|
3
3
|
|
|
4
4
|
export const SiteFooter = () => {
|
|
5
5
|
return (
|
|
6
|
-
<footer className="
|
|
7
|
-
<div className="text-muted-foreground flex flex-col items-center justify-between gap-4 text-sm sm:flex-row">
|
|
6
|
+
<footer className="border-t">
|
|
7
|
+
<div className="text-muted-foreground container flex flex-col items-center justify-between gap-4 py-6 text-sm sm:flex-row">
|
|
8
8
|
<p>
|
|
9
9
|
© {new Date().getUTCFullYear()} {siteConfig.name}. All rights
|
|
10
10
|
reserved.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { createId } from '@paralleldrive/cuid2'
|
|
2
|
-
import { betterAuth } from 'better-auth'
|
|
2
|
+
import { APIError, betterAuth } from 'better-auth'
|
|
3
3
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
admin,
|
|
6
|
+
createAuthMiddleware,
|
|
7
|
+
lastLoginMethod,
|
|
8
|
+
magicLink
|
|
9
|
+
} from 'better-auth/plugins'
|
|
10
|
+
import { isFakeEmail } from 'fakefilter'
|
|
5
11
|
import { headers } from 'next/headers'
|
|
6
12
|
import { cache } from 'react'
|
|
7
13
|
import { db } from '~/db'
|
|
@@ -57,7 +63,20 @@ export const auth = betterAuth({
|
|
|
57
63
|
}),
|
|
58
64
|
admin(),
|
|
59
65
|
lastLoginMethod()
|
|
60
|
-
]
|
|
66
|
+
],
|
|
67
|
+
hooks: {
|
|
68
|
+
before: createAuthMiddleware(async ctx => {
|
|
69
|
+
if (
|
|
70
|
+
ctx.path === '/sign-in/magic-link' &&
|
|
71
|
+
ctx.body?.email &&
|
|
72
|
+
isFakeEmail(ctx.body.email)
|
|
73
|
+
) {
|
|
74
|
+
throw new APIError('BAD_REQUEST', {
|
|
75
|
+
message: 'Please use a valid email address.'
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
61
80
|
})
|
|
62
81
|
|
|
63
82
|
export const getAuthSession = cache(async () => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
type UseMutationOptions,
|
|
7
|
+
type UseQueryOptions
|
|
8
|
+
} from '@tanstack/react-query'
|
|
9
|
+
import type { SafeActionResult } from 'next-safe-action'
|
|
10
|
+
|
|
11
|
+
type AnyAction = (
|
|
12
|
+
...args: any[]
|
|
13
|
+
) => Promise<SafeActionResult<any, any, any, any, any>>
|
|
14
|
+
type ActionInput<T extends AnyAction> = Parameters<T>[0]
|
|
15
|
+
type ActionData<T extends AnyAction> = Awaited<ReturnType<T>>['data']
|
|
16
|
+
|
|
17
|
+
class SafeActionError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
public message: string,
|
|
20
|
+
public validationErrors?: Record<string, string[] | undefined>
|
|
21
|
+
) {
|
|
22
|
+
super(message)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useSafeActionQuery<TAction extends AnyAction>(
|
|
27
|
+
key: string,
|
|
28
|
+
action: TAction,
|
|
29
|
+
input: ActionInput<TAction>,
|
|
30
|
+
options?: Omit<UseQueryOptions<ActionData<TAction>>, 'queryKey' | 'queryFn'>
|
|
31
|
+
) {
|
|
32
|
+
return useQuery({
|
|
33
|
+
queryKey: [key, input],
|
|
34
|
+
queryFn: async () => {
|
|
35
|
+
const result = await action(input)
|
|
36
|
+
|
|
37
|
+
if (result?.serverError) {
|
|
38
|
+
throw new SafeActionError(result.serverError)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result?.validationErrors) {
|
|
42
|
+
throw new SafeActionError('Validation failed', result.validationErrors)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result?.data
|
|
46
|
+
},
|
|
47
|
+
...options
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useSafeActionMutation<TAction extends AnyAction>(
|
|
52
|
+
action: TAction,
|
|
53
|
+
options?: UseMutationOptions<
|
|
54
|
+
ActionData<TAction>,
|
|
55
|
+
SafeActionError,
|
|
56
|
+
ActionInput<TAction>
|
|
57
|
+
>
|
|
58
|
+
) {
|
|
59
|
+
return useMutation({
|
|
60
|
+
mutationFn: async (input: ActionInput<TAction>) => {
|
|
61
|
+
const result = await action(input)
|
|
62
|
+
|
|
63
|
+
if (result?.serverError) {
|
|
64
|
+
throw new SafeActionError(result.serverError)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (result?.validationErrors) {
|
|
68
|
+
throw new SafeActionError('Validation failed', result.validationErrors)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result?.data
|
|
72
|
+
},
|
|
73
|
+
...options
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -8,8 +8,8 @@ export const toastSuccessMessage = (message: string) =>
|
|
|
8
8
|
position: 'top-center'
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
export const toastErrorMessage = () =>
|
|
12
|
-
toast.error('Something went wrong. Please try again later.', {
|
|
11
|
+
export const toastErrorMessage = (message?: string) =>
|
|
12
|
+
toast.error(message || 'Something went wrong. Please try again later.', {
|
|
13
13
|
richColors: true,
|
|
14
14
|
position: 'top-center'
|
|
15
15
|
})
|