@webdevarif/dashui 0.4.0 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdevarif/dashui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Universal dashboard UI component library — forms, inputs, media, tables, layouts. Modular categories: primitives, forms, dashboard, media, data, editors, ecommerce, cms.",
5
5
  "keywords": [
6
6
  "dashboard",
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "files": [
36
36
  "dist",
37
+ "templates",
37
38
  "README.md"
38
39
  ],
39
40
  "scripts": {
@@ -0,0 +1,74 @@
1
+ # dashui Templates
2
+
3
+ Copy-paste starter code for your Next.js project. Customize to your API/database.
4
+
5
+ ## What's included
6
+
7
+ - **nextauth/** — NextAuth v6 edge-safe configuration + route handlers
8
+ - **prisma/** — Base multi-tenant Prisma schemas (SaaS, E-commerce, CMS)
9
+ - **hooks/** — SWR data fetching patterns (useFetch, useCreate, useUpdate, useDelete)
10
+ - **middleware/** — Auth middleware, error handling, rate limiting
11
+ - **api-patterns/** — CRUD route templates, file uploads, webhooks
12
+
13
+ ## Quick start
14
+
15
+ ### 1. Copy NextAuth config
16
+ ```bash
17
+ cp templates/nextauth/auth.config.ts ./
18
+ cp templates/nextauth/auth.ts ./
19
+ cp -r templates/nextauth/route-handlers ./app/api/auth/
20
+ ```
21
+
22
+ Then customize your provider credentials in `.env`.
23
+
24
+ ### 2. Copy Prisma schema
25
+ ```bash
26
+ cp templates/prisma/multi-tenant-schema.prisma ./prisma/schema.prisma
27
+ ```
28
+
29
+ Add your custom models.
30
+
31
+ ### 3. Copy data fetching hooks
32
+ ```bash
33
+ cp templates/hooks/*.ts ./lib/
34
+ ```
35
+
36
+ Use in components:
37
+ ```tsx
38
+ const { data, error, isLoading } = useFetch('/api/products')
39
+ ```
40
+
41
+ ### 4. Copy middleware
42
+ ```bash
43
+ cp templates/middleware/auth-middleware.ts ./lib/
44
+ ```
45
+
46
+ Use in API routes:
47
+ ```tsx
48
+ import { requireAuth } from '@/lib/auth-middleware'
49
+
50
+ export async function POST(req) {
51
+ const user = await requireAuth(req)
52
+ // user is authenticated
53
+ }
54
+ ```
55
+
56
+ ## Before publishing
57
+
58
+ Each template is a starting point. **Customize to your needs:**
59
+ - Environment variables (OAuth credentials, database URL, API keys)
60
+ - Prisma relations (add your custom models)
61
+ - API endpoints (match your backend structure)
62
+ - Error handling (log to your service)
63
+ - Rate limits (adjust for your use case)
64
+
65
+ ## FAQ
66
+
67
+ **Q: Can I modify these templates?**
68
+ A: Yes! They're in your project now. Make them yours.
69
+
70
+ **Q: Why not a generator/CLI?**
71
+ A: Copy-paste is simpler, more transparent, easier to customize. You see exactly what's installed.
72
+
73
+ **Q: How do I keep up with dashui updates?**
74
+ A: Components update → just re-export from dashui. Templates are static starter code — no updates needed unless you want them.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * CRUD API Route Pattern
3
+ *
4
+ * Copy and modify for your models.
5
+ * This example shows: GET, POST, PATCH, DELETE
6
+ */
7
+
8
+ import { NextRequest, NextResponse } from 'next/server'
9
+ import { requireAuth, requireRole } from '@/lib/auth-middleware'
10
+ import { prisma } from '@/lib/prisma'
11
+
12
+ // GET — List with pagination + filters
13
+ export async function GET(req: NextRequest) {
14
+ try {
15
+ const user = await requireAuth(req)
16
+
17
+ const url = new URL(req.url)
18
+ const page = parseInt(url.searchParams.get('page') || '1')
19
+ const limit = parseInt(url.searchParams.get('limit') || '20')
20
+ const skip = (page - 1) * limit
21
+
22
+ // TODO: Add your filters/search logic here
23
+ const items = await prisma.product.findMany({
24
+ skip,
25
+ take: limit,
26
+ // where: { storeId: user.storeId }, // filter by user's store if applicable
27
+ })
28
+
29
+ const total = await prisma.product.count()
30
+
31
+ return NextResponse.json({
32
+ data: items,
33
+ pagination: { page, limit, total, pages: Math.ceil(total / limit) },
34
+ })
35
+ } catch (err) {
36
+ console.error('GET error:', err)
37
+ return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
38
+ }
39
+ }
40
+
41
+ // POST — Create
42
+ export async function POST(req: NextRequest) {
43
+ try {
44
+ const user = await requireAuth(req)
45
+ const body = await req.json()
46
+
47
+ // TODO: Validate body with zod or similar
48
+ if (!body.name) {
49
+ return NextResponse.json({ error: 'Name required' }, { status: 400 })
50
+ }
51
+
52
+ // TODO: Check permissions (user can create?)
53
+ const item = await prisma.product.create({
54
+ data: {
55
+ name: body.name,
56
+ description: body.description,
57
+ price: body.price,
58
+ // storeId: user.storeId, // if multi-tenant
59
+ },
60
+ })
61
+
62
+ return NextResponse.json(item, { status: 201 })
63
+ } catch (err) {
64
+ console.error('POST error:', err)
65
+ return NextResponse.json({ error: 'Failed to create' }, { status: 500 })
66
+ }
67
+ }
68
+
69
+ // PATCH — Update
70
+ export async function PATCH(req: NextRequest) {
71
+ try {
72
+ const user = await requireAuth(req)
73
+ const body = await req.json()
74
+ const { id, ...data } = body
75
+
76
+ if (!id) {
77
+ return NextResponse.json({ error: 'ID required' }, { status: 400 })
78
+ }
79
+
80
+ // TODO: Check permissions (user owns this item?)
81
+ const item = await prisma.product.update({
82
+ where: { id },
83
+ data,
84
+ })
85
+
86
+ return NextResponse.json(item)
87
+ } catch (err) {
88
+ console.error('PATCH error:', err)
89
+ return NextResponse.json({ error: 'Failed to update' }, { status: 500 })
90
+ }
91
+ }
92
+
93
+ // DELETE
94
+ export async function DELETE(req: NextRequest) {
95
+ try {
96
+ const user = await requireAuth(req)
97
+ const { searchParams } = new URL(req.url)
98
+ const id = searchParams.get('id')
99
+
100
+ if (!id) {
101
+ return NextResponse.json({ error: 'ID required' }, { status: 400 })
102
+ }
103
+
104
+ // TODO: Check permissions (user owns this item?)
105
+ await prisma.product.delete({ where: { id } })
106
+
107
+ return NextResponse.json({ success: true })
108
+ } catch (err) {
109
+ console.error('DELETE error:', err)
110
+ return NextResponse.json({ error: 'Failed to delete' }, { status: 500 })
111
+ }
112
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * useCreate — POST request with optimistic update
3
+ *
4
+ * Copy to: ./lib/hooks/useCreate.ts
5
+ *
6
+ * Usage:
7
+ * const { mutate, isLoading, error } = useCreate('/api/products')
8
+ * await mutate({ name: 'New Product', price: 99.99 })
9
+ */
10
+
11
+ import { useState } from 'react'
12
+
13
+ export function useCreate<T = any>(endpoint: string) {
14
+ const [isLoading, setIsLoading] = useState(false)
15
+ const [error, setError] = useState<Error | null>(null)
16
+
17
+ async function mutate(data: any) {
18
+ setIsLoading(true)
19
+ setError(null)
20
+
21
+ try {
22
+ const response = await fetch(endpoint, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ body: JSON.stringify(data),
28
+ })
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`Create failed: ${response.statusText}`)
32
+ }
33
+
34
+ const result = await response.json()
35
+ return result as T
36
+ } catch (err) {
37
+ const error = err instanceof Error ? err : new Error('Unknown error')
38
+ setError(error)
39
+ throw error
40
+ } finally {
41
+ setIsLoading(false)
42
+ }
43
+ }
44
+
45
+ return { mutate, isLoading, error }
46
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * useDelete — DELETE request
3
+ *
4
+ * Copy to: ./lib/hooks/useDelete.ts
5
+ *
6
+ * Usage:
7
+ * const { mutate, isLoading, error } = useDelete('/api/products/123')
8
+ * await mutate()
9
+ */
10
+
11
+ import { useState } from 'react'
12
+
13
+ export function useDelete<T = any>(endpoint: string) {
14
+ const [isLoading, setIsLoading] = useState(false)
15
+ const [error, setError] = useState<Error | null>(null)
16
+
17
+ async function mutate() {
18
+ setIsLoading(true)
19
+ setError(null)
20
+
21
+ try {
22
+ const response = await fetch(endpoint, {
23
+ method: 'DELETE',
24
+ })
25
+
26
+ if (!response.ok) {
27
+ throw new Error(`Delete failed: ${response.statusText}`)
28
+ }
29
+
30
+ const result = await response.json()
31
+ return result as T
32
+ } catch (err) {
33
+ const error = err instanceof Error ? err : new Error('Unknown error')
34
+ setError(error)
35
+ throw error
36
+ } finally {
37
+ setIsLoading(false)
38
+ }
39
+ }
40
+
41
+ return { mutate, isLoading, error }
42
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * useFetch — Generic SWR wrapper for GET requests
3
+ *
4
+ * Copy to: ./lib/hooks/useFetch.ts
5
+ *
6
+ * Usage:
7
+ * const { data, error, isLoading } = useFetch('/api/products')
8
+ */
9
+
10
+ import useSWR from 'swr'
11
+
12
+ const fetcher = (url: string) => fetch(url).then((r) => r.json())
13
+
14
+ export function useFetch<T = any>(endpoint: string | null) {
15
+ const { data, error, isLoading } = useSWR<T>(
16
+ endpoint, // null = skip fetching
17
+ fetcher,
18
+ {
19
+ revalidateOnFocus: false,
20
+ revalidateOnReconnect: false,
21
+ dedupingInterval: 60000,
22
+ }
23
+ )
24
+
25
+ return {
26
+ data,
27
+ error,
28
+ isLoading,
29
+ }
30
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * useUpdate — PATCH request with optimistic update
3
+ *
4
+ * Copy to: ./lib/hooks/useUpdate.ts
5
+ *
6
+ * Usage:
7
+ * const { mutate, isLoading, error } = useUpdate('/api/products/123')
8
+ * await mutate({ name: 'Updated Product' })
9
+ */
10
+
11
+ import { useState } from 'react'
12
+
13
+ export function useUpdate<T = any>(endpoint: string) {
14
+ const [isLoading, setIsLoading] = useState(false)
15
+ const [error, setError] = useState<Error | null>(null)
16
+
17
+ async function mutate(data: any) {
18
+ setIsLoading(true)
19
+ setError(null)
20
+
21
+ try {
22
+ const response = await fetch(endpoint, {
23
+ method: 'PATCH',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ body: JSON.stringify(data),
28
+ })
29
+
30
+ if (!response.ok) {
31
+ throw new Error(`Update failed: ${response.statusText}`)
32
+ }
33
+
34
+ const result = await response.json()
35
+ return result as T
36
+ } catch (err) {
37
+ const error = err instanceof Error ? err : new Error('Unknown error')
38
+ setError(error)
39
+ throw error
40
+ } finally {
41
+ setIsLoading(false)
42
+ }
43
+ }
44
+
45
+ return { mutate, isLoading, error }
46
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * requireAuth — API route middleware to enforce authentication
3
+ *
4
+ * Copy to: ./lib/auth-middleware.ts
5
+ *
6
+ * Usage in API routes:
7
+ * export async function POST(req: Request) {
8
+ * const user = await requireAuth(req)
9
+ * // user is authenticated; access user.id, user.email, etc.
10
+ * }
11
+ */
12
+
13
+ import { auth } from '@/auth'
14
+ import { NextRequest } from 'next/server'
15
+
16
+ export async function requireAuth(req: NextRequest) {
17
+ const session = await auth()
18
+
19
+ if (!session || !session.user) {
20
+ throw new Error('Unauthorized: Not authenticated', { cause: 'UNAUTHENTICATED' })
21
+ }
22
+
23
+ return session.user
24
+ }
25
+
26
+ export async function requireRole(req: NextRequest, requiredRole: string) {
27
+ const user = await requireAuth(req)
28
+
29
+ const role = (user as any).role || 'user'
30
+
31
+ // Simple role check (admin can do everything)
32
+ if (role === 'admin') return user
33
+
34
+ if (role !== requiredRole) {
35
+ throw new Error(`Forbidden: Requires role ${requiredRole}`, { cause: 'FORBIDDEN' })
36
+ }
37
+
38
+ return user
39
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * NextAuth v6 Edge-safe Config
3
+ *
4
+ * Copy to: ./auth.config.ts
5
+ * Customize: Add your OAuth provider credentials to .env
6
+ *
7
+ * Providers:
8
+ * - Credentials (username/password)
9
+ * - GitHub (OAuth)
10
+ * - Google (OAuth)
11
+ * - Add more as needed
12
+ */
13
+
14
+ import type { NextAuthConfig } from 'next-auth'
15
+ import Credentials from 'next-auth/providers/credentials'
16
+ import GitHub from 'next-auth/providers/github'
17
+ import Google from 'next-auth/providers/google'
18
+ import { PrismaAdapter } from '@auth/prisma-adapter'
19
+ import { prisma } from '@/lib/prisma'
20
+
21
+ export const authConfig = {
22
+ providers: [
23
+ // Credentials provider (username/password)
24
+ Credentials({
25
+ async authorize(credentials) {
26
+ if (!credentials?.email || !credentials?.password) {
27
+ return null
28
+ }
29
+
30
+ // TODO: Implement your credential validation logic
31
+ // Example: find user, verify password hash, return user
32
+ const user = await prisma.user.findUnique({
33
+ where: { email: credentials.email as string },
34
+ })
35
+
36
+ if (!user) return null
37
+
38
+ // Verify password (use bcrypt or similar)
39
+ // const isValid = await bcrypt.compare(credentials.password, user.password)
40
+ // if (!isValid) return null
41
+
42
+ return {
43
+ id: user.id,
44
+ email: user.email,
45
+ name: user.name,
46
+ image: user.image,
47
+ }
48
+ },
49
+ }),
50
+
51
+ // GitHub OAuth (register app: https://github.com/settings/apps)
52
+ GitHub({
53
+ clientId: process.env.GITHUB_ID,
54
+ clientSecret: process.env.GITHUB_SECRET,
55
+ }),
56
+
57
+ // Google OAuth (create: https://console.cloud.google.com/)
58
+ Google({
59
+ clientId: process.env.GOOGLE_ID,
60
+ clientSecret: process.env.GOOGLE_SECRET,
61
+ }),
62
+ ],
63
+
64
+ adapter: PrismaAdapter(prisma),
65
+
66
+ pages: {
67
+ signIn: '/login',
68
+ error: '/login',
69
+ },
70
+
71
+ callbacks: {
72
+ // Add custom logic when user signs in
73
+ async signIn({ user, account, profile, email, credentials }) {
74
+ // Check if user is allowed, verify email domain, etc.
75
+ return true
76
+ },
77
+
78
+ // Add user role/permissions to session
79
+ async session({ session, user }) {
80
+ return {
81
+ ...session,
82
+ user: {
83
+ ...session.user,
84
+ id: user.id,
85
+ role: (user as any).role || 'user', // Adjust based on your schema
86
+ },
87
+ }
88
+ },
89
+
90
+ // Add user info to JWT token
91
+ async jwt({ token, user, account }) {
92
+ if (user) {
93
+ token.id = user.id
94
+ token.role = (user as any).role || 'user'
95
+ }
96
+ return token
97
+ },
98
+ },
99
+
100
+ events: {
101
+ // Log important events
102
+ async signIn({ user, account, profile, isNewUser }) {
103
+ console.log(`User signed in: ${user.email}`)
104
+ },
105
+
106
+ async signOut({ token }) {
107
+ console.log(`User signed out`)
108
+ },
109
+ },
110
+ } satisfies NextAuthConfig
@@ -0,0 +1,11 @@
1
+ /**
2
+ * NextAuth v6 Instance
3
+ *
4
+ * Copy to: ./auth.ts
5
+ * Wraps auth.config.ts with handlers for API routes, middleware, client hooks
6
+ */
7
+
8
+ import NextAuth from 'next-auth'
9
+ import { authConfig } from './auth.config'
10
+
11
+ export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)
@@ -0,0 +1,10 @@
1
+ /**
2
+ * NextAuth API Route Handler
3
+ *
4
+ * Copy to: ./app/api/auth/[auth]/route.ts
5
+ * This handles all NextAuth routes: signIn, signOut, callback, etc.
6
+ */
7
+
8
+ import { handlers } from '@/auth'
9
+
10
+ export const { GET, POST } = handlers
@@ -0,0 +1,165 @@
1
+ // This is your Prisma schema file.
2
+ // Learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ datasource db {
5
+ provider = "postgresql"
6
+ url = env("DATABASE_URL")
7
+ }
8
+
9
+ generator client {
10
+ provider = "prisma-client-js"
11
+ }
12
+
13
+ // ─── NextAuth Models ────────────────────────────────────────────────────────
14
+ // Auto-generated by NextAuth + Prisma Adapter
15
+
16
+ model Account {
17
+ id String @id @default(cuid())
18
+ userId String
19
+ type String
20
+ provider String
21
+ providerAccountId String
22
+ refresh_token String? @db.Text
23
+ access_token String? @db.Text
24
+ expires_at Int?
25
+ token_type String?
26
+ scope String?
27
+ id_token String? @db.Text
28
+ session_state String?
29
+
30
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
31
+
32
+ @@unique([provider, providerAccountId])
33
+ }
34
+
35
+ model Session {
36
+ id String @id @default(cuid())
37
+ sessionToken String @unique
38
+ userId String
39
+ expires DateTime
40
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
41
+ }
42
+
43
+ model VerificationToken {
44
+ identifier String
45
+ token String @unique
46
+ expires DateTime
47
+
48
+ @@unique([identifier, token])
49
+ }
50
+
51
+ // ─── User & Auth ────────────────────────────────────────────────────────────
52
+
53
+ model User {
54
+ id String @id @default(cuid())
55
+ name String?
56
+ email String @unique
57
+ emailVerified DateTime?
58
+ password String? // Only for credentials provider
59
+ image String?
60
+ role String @default("user") // "admin", "user", "moderator"
61
+ createdAt DateTime @default(now())
62
+ updatedAt DateTime @updatedAt
63
+
64
+ // Relations
65
+ accounts Account[]
66
+ sessions Session[]
67
+ stores Store[] @relation("storeOwner")
68
+ members StoreMember[]
69
+ posts Post[]
70
+ }
71
+
72
+ // ─── Multi-Tenant: Store/Organization ──────────────────────────────────────
73
+
74
+ model Store {
75
+ id String @id @default(cuid())
76
+ name String
77
+ slug String @unique
78
+ ownerId String
79
+ owner User @relation("storeOwner", fields: [ownerId], references: [id], onDelete: Cascade)
80
+ description String?
81
+ logo String?
82
+ settings Json? // Store-specific settings
83
+ createdAt DateTime @default(now())
84
+ updatedAt DateTime @updatedAt
85
+
86
+ // Relations
87
+ members StoreMember[]
88
+ posts Post[]
89
+ products Product[]
90
+ }
91
+
92
+ model StoreMember {
93
+ id String @id @default(cuid())
94
+ storeId String
95
+ userId String
96
+ role String @default("member") // "admin", "editor", "member", "viewer"
97
+
98
+ store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
99
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
100
+
101
+ @@unique([storeId, userId])
102
+ }
103
+
104
+ // ─── Content: Posts/Pages ──────────────────────────────────────────────────
105
+
106
+ model Post {
107
+ id String @id @default(cuid())
108
+ storeId String
109
+ title String
110
+ slug String
111
+ content String @db.Text
112
+ status String @default("draft") // "draft", "published", "scheduled", "archived"
113
+ type String @default("post") // "post", "page", "custom"
114
+ excerpt String?
115
+ image String?
116
+ authorId String
117
+ createdAt DateTime @default(now())
118
+ updatedAt DateTime @updatedAt
119
+
120
+ store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
121
+ author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
122
+
123
+ @@unique([storeId, slug])
124
+ @@index([storeId])
125
+ @@index([authorId])
126
+ }
127
+
128
+ // ─── E-Commerce: Products ────────────────────────────────────────────────
129
+
130
+ model Product {
131
+ id String @id @default(cuid())
132
+ storeId String
133
+ name String
134
+ description String? @db.Text
135
+ price Decimal @db.Decimal(10, 2)
136
+ image String?
137
+ published Boolean @default(false)
138
+ createdAt DateTime @default(now())
139
+ updatedAt DateTime @updatedAt
140
+
141
+ store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
142
+
143
+ @@index([storeId])
144
+ }
145
+
146
+ // ─── Extend with your own models ────────────────────────────────────────────
147
+
148
+ // Example: E-commerce orders
149
+ // model Order {
150
+ // id String @id @default(cuid())
151
+ // storeId String
152
+ // userId String
153
+ // total Decimal
154
+ // status String @default("pending")
155
+ // createdAt DateTime @default(now())
156
+ // }
157
+
158
+ // Example: Blog comments
159
+ // model Comment {
160
+ // id String @id @default(cuid())
161
+ // postId String
162
+ // userId String
163
+ // content String @db.Text
164
+ // createdAt DateTime @default(now())
165
+ // }