create-n8-app 0.1.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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +239 -0
- package/package.json +58 -0
- package/template/_env.example +34 -0
- package/template/_env.local +31 -0
- package/template/_eslintrc.json +3 -0
- package/template/_gitignore +46 -0
- package/template/_package.json +59 -0
- package/template/app/api/auth/[...nextauth]/route.ts +3 -0
- package/template/app/api/trpc/[trpc]/route.ts +19 -0
- package/template/app/auth/signin/page.tsx +39 -0
- package/template/app/globals.css +33 -0
- package/template/app/layout.tsx +28 -0
- package/template/app/page.tsx +68 -0
- package/template/app/providers.tsx +47 -0
- package/template/components/auth/user-button.tsx +46 -0
- package/template/components/ui/.gitkeep +2 -0
- package/template/components.json +21 -0
- package/template/drizzle/.gitkeep +1 -0
- package/template/drizzle.config.ts +10 -0
- package/template/hooks/.gitkeep +1 -0
- package/template/lib/auth.ts +44 -0
- package/template/lib/db.ts +10 -0
- package/template/lib/env.ts +76 -0
- package/template/lib/trpc.ts +4 -0
- package/template/lib/utils.ts +6 -0
- package/template/next-env.d.ts +5 -0
- package/template/next.config.ts +14 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prettier.config.js +11 -0
- package/template/public/.gitkeep +1 -0
- package/template/server/api/root.ts +22 -0
- package/template/server/api/routers/example.ts +76 -0
- package/template/server/api/trpc.ts +67 -0
- package/template/server/db/schema.ts +95 -0
- package/template/stores/example-store.ts +121 -0
- package/template/tests/example.test.ts +22 -0
- package/template/tests/setup.ts +22 -0
- package/template/tsconfig.json +27 -0
- package/template/vitest.config.ts +30 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pgTable,
|
|
3
|
+
text,
|
|
4
|
+
timestamp,
|
|
5
|
+
varchar,
|
|
6
|
+
serial,
|
|
7
|
+
integer,
|
|
8
|
+
primaryKey,
|
|
9
|
+
} from 'drizzle-orm/pg-core'
|
|
10
|
+
import type { AdapterAccountType } from 'next-auth/adapters'
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Auth Tables (NextAuth.js / Auth.js)
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export const users = pgTable('users', {
|
|
17
|
+
id: text('id')
|
|
18
|
+
.primaryKey()
|
|
19
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
20
|
+
name: text('name'),
|
|
21
|
+
email: text('email').unique(),
|
|
22
|
+
emailVerified: timestamp('email_verified', { mode: 'date' }),
|
|
23
|
+
image: text('image'),
|
|
24
|
+
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
|
25
|
+
updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const accounts = pgTable(
|
|
29
|
+
'accounts',
|
|
30
|
+
{
|
|
31
|
+
userId: text('user_id')
|
|
32
|
+
.notNull()
|
|
33
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
34
|
+
type: text('type').$type<AdapterAccountType>().notNull(),
|
|
35
|
+
provider: text('provider').notNull(),
|
|
36
|
+
providerAccountId: text('provider_account_id').notNull(),
|
|
37
|
+
refresh_token: text('refresh_token'),
|
|
38
|
+
access_token: text('access_token'),
|
|
39
|
+
expires_at: integer('expires_at'),
|
|
40
|
+
token_type: text('token_type'),
|
|
41
|
+
scope: text('scope'),
|
|
42
|
+
id_token: text('id_token'),
|
|
43
|
+
session_state: text('session_state'),
|
|
44
|
+
},
|
|
45
|
+
(account) => [
|
|
46
|
+
primaryKey({
|
|
47
|
+
columns: [account.provider, account.providerAccountId],
|
|
48
|
+
}),
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
export const sessions = pgTable('sessions', {
|
|
53
|
+
sessionToken: text('session_token').primaryKey(),
|
|
54
|
+
userId: text('user_id')
|
|
55
|
+
.notNull()
|
|
56
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
57
|
+
expires: timestamp('expires', { mode: 'date' }).notNull(),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export const verificationTokens = pgTable(
|
|
61
|
+
'verification_tokens',
|
|
62
|
+
{
|
|
63
|
+
identifier: text('identifier').notNull(),
|
|
64
|
+
token: text('token').notNull(),
|
|
65
|
+
expires: timestamp('expires', { mode: 'date' }).notNull(),
|
|
66
|
+
},
|
|
67
|
+
(verificationToken) => [
|
|
68
|
+
primaryKey({
|
|
69
|
+
columns: [verificationToken.identifier, verificationToken.token],
|
|
70
|
+
}),
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Example Application Tables
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
export const posts = pgTable('posts', {
|
|
79
|
+
id: serial('id').primaryKey(),
|
|
80
|
+
title: varchar('title', { length: 256 }).notNull(),
|
|
81
|
+
content: text('content'),
|
|
82
|
+
authorId: text('author_id').references(() => users.id, { onDelete: 'set null' }),
|
|
83
|
+
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
|
|
84
|
+
updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Type Exports
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
export type User = typeof users.$inferSelect
|
|
92
|
+
export type NewUser = typeof users.$inferInsert
|
|
93
|
+
|
|
94
|
+
export type Post = typeof posts.$inferSelect
|
|
95
|
+
export type NewPost = typeof posts.$inferInsert
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import { persist } from 'zustand/middleware'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example Zustand store for managing global UI state
|
|
6
|
+
*
|
|
7
|
+
* Use Zustand for:
|
|
8
|
+
* - Shopping cart state
|
|
9
|
+
* - UI state (modals, sidebars, theme)
|
|
10
|
+
* - User preferences and settings
|
|
11
|
+
* - Global app state shared across components
|
|
12
|
+
*
|
|
13
|
+
* DON'T use Zustand for:
|
|
14
|
+
* - Server data (use React Query instead)
|
|
15
|
+
* - Form state (use React Hook Form or local state)
|
|
16
|
+
* - Local component state (use useState)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface UIState {
|
|
20
|
+
// Sidebar state
|
|
21
|
+
sidebarOpen: boolean
|
|
22
|
+
toggleSidebar: () => void
|
|
23
|
+
setSidebarOpen: (open: boolean) => void
|
|
24
|
+
|
|
25
|
+
// Theme state
|
|
26
|
+
theme: 'light' | 'dark' | 'system'
|
|
27
|
+
setTheme: (theme: 'light' | 'dark' | 'system') => void
|
|
28
|
+
|
|
29
|
+
// Modal state
|
|
30
|
+
activeModal: string | null
|
|
31
|
+
openModal: (modalId: string) => void
|
|
32
|
+
closeModal: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useUIStore = create<UIState>()(
|
|
36
|
+
persist(
|
|
37
|
+
(set) => ({
|
|
38
|
+
// Sidebar
|
|
39
|
+
sidebarOpen: true,
|
|
40
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
41
|
+
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
|
42
|
+
|
|
43
|
+
// Theme
|
|
44
|
+
theme: 'system',
|
|
45
|
+
setTheme: (theme) => set({ theme }),
|
|
46
|
+
|
|
47
|
+
// Modal
|
|
48
|
+
activeModal: null,
|
|
49
|
+
openModal: (modalId) => set({ activeModal: modalId }),
|
|
50
|
+
closeModal: () => set({ activeModal: null }),
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
name: 'ui-storage', // localStorage key
|
|
54
|
+
partialize: (state) => ({
|
|
55
|
+
// Only persist these fields
|
|
56
|
+
sidebarOpen: state.sidebarOpen,
|
|
57
|
+
theme: state.theme,
|
|
58
|
+
}),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Example cart store (common use case)
|
|
65
|
+
*/
|
|
66
|
+
interface CartItem {
|
|
67
|
+
id: string
|
|
68
|
+
name: string
|
|
69
|
+
price: number
|
|
70
|
+
quantity: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CartState {
|
|
74
|
+
items: CartItem[]
|
|
75
|
+
addItem: (item: Omit<CartItem, 'quantity'>) => void
|
|
76
|
+
removeItem: (id: string) => void
|
|
77
|
+
updateQuantity: (id: string, quantity: number) => void
|
|
78
|
+
clearCart: () => void
|
|
79
|
+
totalItems: () => number
|
|
80
|
+
totalPrice: () => number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const useCartStore = create<CartState>()(
|
|
84
|
+
persist(
|
|
85
|
+
(set, get) => ({
|
|
86
|
+
items: [],
|
|
87
|
+
|
|
88
|
+
addItem: (item) =>
|
|
89
|
+
set((state) => {
|
|
90
|
+
const existingItem = state.items.find((i) => i.id === item.id)
|
|
91
|
+
if (existingItem) {
|
|
92
|
+
return {
|
|
93
|
+
items: state.items.map((i) =>
|
|
94
|
+
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { items: [...state.items, { ...item, quantity: 1 }] }
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
removeItem: (id) =>
|
|
102
|
+
set((state) => ({
|
|
103
|
+
items: state.items.filter((i) => i.id !== id),
|
|
104
|
+
})),
|
|
105
|
+
|
|
106
|
+
updateQuantity: (id, quantity) =>
|
|
107
|
+
set((state) => ({
|
|
108
|
+
items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
|
|
109
|
+
})),
|
|
110
|
+
|
|
111
|
+
clearCart: () => set({ items: [] }),
|
|
112
|
+
|
|
113
|
+
totalItems: () => get().items.reduce((sum, item) => sum + item.quantity, 0),
|
|
114
|
+
|
|
115
|
+
totalPrice: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
name: 'cart-storage',
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
describe('Example tests', () => {
|
|
4
|
+
it('should add numbers correctly', () => {
|
|
5
|
+
expect(1 + 1).toBe(2)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('should handle string concatenation', () => {
|
|
9
|
+
expect('Hello' + ' ' + 'World').toBe('Hello World')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should handle arrays', () => {
|
|
13
|
+
const arr = [1, 2, 3]
|
|
14
|
+
expect(arr).toHaveLength(3)
|
|
15
|
+
expect(arr).toContain(2)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should handle async operations', async () => {
|
|
19
|
+
const result = await Promise.resolve('success')
|
|
20
|
+
expect(result).toBe('success')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/vitest'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
// Mock Next.js router
|
|
5
|
+
vi.mock('next/navigation', () => ({
|
|
6
|
+
useRouter: () => ({
|
|
7
|
+
push: vi.fn(),
|
|
8
|
+
replace: vi.fn(),
|
|
9
|
+
prefetch: vi.fn(),
|
|
10
|
+
back: vi.fn(),
|
|
11
|
+
}),
|
|
12
|
+
usePathname: () => '/',
|
|
13
|
+
useSearchParams: () => new URLSearchParams(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
// Mock next-auth
|
|
17
|
+
vi.mock('next-auth/react', () => ({
|
|
18
|
+
useSession: () => ({ data: null, status: 'unauthenticated' }),
|
|
19
|
+
signIn: vi.fn(),
|
|
20
|
+
signOut: vi.fn(),
|
|
21
|
+
SessionProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
22
|
+
}))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
test: {
|
|
8
|
+
environment: 'jsdom',
|
|
9
|
+
globals: true,
|
|
10
|
+
setupFiles: ['./tests/setup.ts'],
|
|
11
|
+
include: ['**/*.{test,spec}.{ts,tsx}'],
|
|
12
|
+
exclude: ['node_modules', '.next', 'dist'],
|
|
13
|
+
coverage: {
|
|
14
|
+
provider: 'v8',
|
|
15
|
+
reporter: ['text', 'json', 'html'],
|
|
16
|
+
exclude: [
|
|
17
|
+
'node_modules/',
|
|
18
|
+
'.next/',
|
|
19
|
+
'**/*.d.ts',
|
|
20
|
+
'tests/setup.ts',
|
|
21
|
+
'vitest.config.ts',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
resolve: {
|
|
26
|
+
alias: {
|
|
27
|
+
'@': path.resolve(__dirname, './'),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
})
|