create-velox-app 0.4.13 → 0.6.23
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/README.md +2 -43
- package/dist/cli.js +23 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/templates/auth.d.ts.map +1 -1
- package/dist/templates/auth.js +24 -0
- package/dist/templates/auth.js.map +1 -1
- package/dist/templates/fullstack.d.ts +15 -0
- package/dist/templates/fullstack.d.ts.map +1 -0
- package/dist/templates/fullstack.js +110 -0
- package/dist/templates/fullstack.js.map +1 -0
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +27 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/placeholders.d.ts +6 -1
- package/dist/templates/placeholders.d.ts.map +1 -1
- package/dist/templates/placeholders.js +15 -5
- package/dist/templates/placeholders.js.map +1 -1
- package/dist/templates/rsc.d.ts +15 -0
- package/dist/templates/rsc.d.ts.map +1 -0
- package/dist/templates/rsc.js +192 -0
- package/dist/templates/rsc.js.map +1 -0
- package/dist/templates/shared/root.d.ts +1 -0
- package/dist/templates/shared/root.d.ts.map +1 -1
- package/dist/templates/shared/root.js +4 -0
- package/dist/templates/shared/root.js.map +1 -1
- package/dist/templates/spa.d.ts +12 -0
- package/dist/templates/spa.d.ts.map +1 -0
- package/dist/templates/spa.js +101 -0
- package/dist/templates/spa.js.map +1 -0
- package/dist/templates/trpc.d.ts.map +1 -1
- package/dist/templates/trpc.js +16 -0
- package/dist/templates/trpc.js.map +1 -1
- package/dist/templates/types.d.ts +14 -1
- package/dist/templates/types.d.ts.map +1 -1
- package/dist/templates/types.js +35 -10
- package/dist/templates/types.js.map +1 -1
- package/package.json +3 -3
- package/src/templates/source/api/config/auth.ts +2 -2
- package/src/templates/source/api/config/database.ts +44 -10
- package/src/templates/source/api/index.auth.ts +10 -16
- package/src/templates/source/api/index.default.ts +10 -9
- package/src/templates/source/api/index.trpc.ts +9 -28
- package/src/templates/source/api/package.auth.json +7 -6
- package/src/templates/source/api/package.default.json +5 -4
- package/src/templates/source/api/prisma/schema.auth.prisma +3 -2
- package/src/templates/source/api/prisma/schema.default.prisma +3 -2
- package/src/templates/source/api/prisma.config.ts +7 -1
- package/src/templates/source/api/procedures/auth.ts +38 -66
- package/src/templates/source/api/procedures/health.ts +4 -9
- package/src/templates/source/api/procedures/users.auth.ts +7 -11
- package/src/templates/source/api/procedures/users.default.ts +7 -11
- package/src/templates/source/api/router.auth.ts +31 -0
- package/src/templates/source/api/router.default.ts +29 -0
- package/src/templates/source/api/router.trpc.ts +37 -0
- package/src/templates/source/api/router.types.auth.ts +88 -0
- package/src/templates/source/api/router.types.default.ts +73 -0
- package/src/templates/source/api/router.types.trpc.ts +73 -0
- package/src/templates/source/api/routes.auth.ts +66 -0
- package/src/templates/source/api/routes.default.ts +53 -0
- package/src/templates/source/api/schemas/auth.ts +79 -0
- package/src/templates/source/api/schemas/health.ts +21 -0
- package/src/templates/source/api/schemas/user.ts +62 -12
- package/src/templates/source/api/utils/auth.ts +157 -0
- package/src/templates/source/root/.cursorrules +187 -0
- package/src/templates/source/root/CLAUDE.auth.md +264 -0
- package/src/templates/source/root/CLAUDE.default.md +185 -0
- package/src/templates/source/root/package.json +7 -1
- package/src/templates/source/rsc/CLAUDE.md +104 -0
- package/src/templates/source/rsc/app/actions/posts.ts +93 -0
- package/src/templates/source/rsc/app/actions/users.ts +83 -0
- package/src/templates/source/rsc/app/layouts/dashboard.tsx +127 -0
- package/src/templates/source/rsc/app/layouts/marketing.tsx +25 -0
- package/src/templates/source/rsc/app/layouts/minimal.tsx +30 -0
- package/src/templates/source/rsc/app/layouts/root.tsx +241 -0
- package/src/templates/source/rsc/app/pages/(dashboard)/profile.tsx +71 -0
- package/src/templates/source/rsc/app/pages/(dashboard)/settings.tsx +104 -0
- package/src/templates/source/rsc/app/pages/(marketing)/about.tsx +52 -0
- package/src/templates/source/rsc/app/pages/_not-found.tsx +149 -0
- package/src/templates/source/rsc/app/pages/docs/[...slug].tsx +211 -0
- package/src/templates/source/rsc/app/pages/index.tsx +50 -0
- package/src/templates/source/rsc/app/pages/print.tsx +80 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/[postId].tsx +89 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/index.tsx +76 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/new.tsx +79 -0
- package/src/templates/source/rsc/app/pages/users/[id].tsx +64 -0
- package/src/templates/source/rsc/app/pages/users/_layout.tsx +104 -0
- package/src/templates/source/rsc/app/pages/users.tsx +44 -0
- package/src/templates/source/rsc/app.config.ts +12 -0
- package/src/templates/source/rsc/env.example +6 -0
- package/src/templates/source/rsc/gitignore +34 -0
- package/src/templates/source/rsc/package.json +41 -0
- package/src/templates/source/rsc/prisma/schema.prisma +34 -0
- package/src/templates/source/rsc/prisma.config.ts +22 -0
- package/src/templates/source/rsc/public/favicon.svg +4 -0
- package/src/templates/source/rsc/src/api/database.ts +72 -0
- package/src/templates/source/rsc/src/api/handler.ts +53 -0
- package/src/templates/source/rsc/src/api/procedures/health.ts +48 -0
- package/src/templates/source/rsc/src/api/procedures/posts.ts +151 -0
- package/src/templates/source/rsc/src/api/procedures/users.ts +87 -0
- package/src/templates/source/rsc/src/api/schemas/post.ts +53 -0
- package/src/templates/source/rsc/src/api/schemas/user.ts +38 -0
- package/src/templates/source/rsc/src/entry.client.tsx +28 -0
- package/src/templates/source/rsc/src/entry.server.tsx +304 -0
- package/src/templates/source/rsc/tsconfig.json +24 -0
- package/src/templates/source/web/App.module.css +1 -1
- package/src/templates/source/web/api.ts +8 -1
- package/src/templates/source/web/main.tsx +4 -4
- package/src/templates/source/web/package.json +6 -6
- package/src/templates/source/web/routes/__root.tsx +2 -2
- package/src/templates/source/web/routes/index.auth.tsx +3 -0
- package/src/templates/source/web/routes/users.tsx +1 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs Catch-All Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component that handles all /docs/* routes.
|
|
5
|
+
* Demonstrates catch-all routing with [...slug] pattern.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* /docs/getting-started -> slug = "getting-started"
|
|
9
|
+
* /docs/api/reference -> slug = "api/reference"
|
|
10
|
+
* /docs/api/reference/types -> slug = "api/reference/types"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface PageProps {
|
|
14
|
+
params: Record<string, string>;
|
|
15
|
+
searchParams: Record<string, string | string[]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Mock documentation structure
|
|
19
|
+
const docsTree: Record<string, { title: string; content: string }> = {
|
|
20
|
+
'getting-started': {
|
|
21
|
+
title: 'Getting Started',
|
|
22
|
+
content: 'Welcome to VeloxTS! This guide will help you get up and running quickly.',
|
|
23
|
+
},
|
|
24
|
+
installation: {
|
|
25
|
+
title: 'Installation',
|
|
26
|
+
content: 'Install VeloxTS using your preferred package manager: pnpm, npm, or yarn.',
|
|
27
|
+
},
|
|
28
|
+
'api/reference': {
|
|
29
|
+
title: 'API Reference',
|
|
30
|
+
content: 'Complete reference for all VeloxTS APIs and utilities.',
|
|
31
|
+
},
|
|
32
|
+
'api/reference/types': {
|
|
33
|
+
title: 'Type Definitions',
|
|
34
|
+
content: 'TypeScript type definitions exported by VeloxTS packages.',
|
|
35
|
+
},
|
|
36
|
+
'guides/routing': {
|
|
37
|
+
title: 'Routing Guide',
|
|
38
|
+
content: 'Learn about file-based routing, dynamic routes, and catch-all patterns.',
|
|
39
|
+
},
|
|
40
|
+
'guides/layouts': {
|
|
41
|
+
title: 'Layouts Guide',
|
|
42
|
+
content: 'Understand how layouts work with route groups and segment inheritance.',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default async function DocsPage({ params }: PageProps) {
|
|
47
|
+
const slug = params.slug || 'getting-started';
|
|
48
|
+
const segments = slug.split('/');
|
|
49
|
+
|
|
50
|
+
// Look up documentation
|
|
51
|
+
const doc = docsTree[slug];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="docs-page">
|
|
55
|
+
<style>{`
|
|
56
|
+
.docs-page {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: 2rem;
|
|
59
|
+
min-height: 60vh;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.docs-sidebar {
|
|
63
|
+
width: 200px;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.docs-sidebar h3 {
|
|
68
|
+
font-size: 0.75rem;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
color: #888;
|
|
71
|
+
margin-bottom: 0.5rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.docs-sidebar ul {
|
|
75
|
+
list-style: none;
|
|
76
|
+
margin-bottom: 1rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.docs-sidebar a {
|
|
80
|
+
display: block;
|
|
81
|
+
padding: 0.25rem 0;
|
|
82
|
+
color: #1a1a2e;
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.docs-sidebar a:hover {
|
|
87
|
+
color: #6366f1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.docs-main {
|
|
91
|
+
flex: 1;
|
|
92
|
+
background: white;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
padding: 2rem;
|
|
95
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.docs-breadcrumb {
|
|
99
|
+
font-size: 0.875rem;
|
|
100
|
+
color: #888;
|
|
101
|
+
margin-bottom: 1rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.docs-breadcrumb a {
|
|
105
|
+
color: #6366f1;
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.docs-content {
|
|
110
|
+
line-height: 1.8;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.docs-meta {
|
|
114
|
+
margin-top: 2rem;
|
|
115
|
+
padding-top: 1rem;
|
|
116
|
+
border-top: 1px solid #eee;
|
|
117
|
+
font-size: 0.875rem;
|
|
118
|
+
color: #888;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.not-found {
|
|
122
|
+
text-align: center;
|
|
123
|
+
padding: 2rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.not-found h1 {
|
|
127
|
+
color: #e11d48;
|
|
128
|
+
}
|
|
129
|
+
`}</style>
|
|
130
|
+
|
|
131
|
+
<aside className="docs-sidebar">
|
|
132
|
+
<h3>Getting Started</h3>
|
|
133
|
+
<ul>
|
|
134
|
+
<li>
|
|
135
|
+
<a href="/docs/getting-started">Getting Started</a>
|
|
136
|
+
</li>
|
|
137
|
+
<li>
|
|
138
|
+
<a href="/docs/installation">Installation</a>
|
|
139
|
+
</li>
|
|
140
|
+
</ul>
|
|
141
|
+
|
|
142
|
+
<h3>API Reference</h3>
|
|
143
|
+
<ul>
|
|
144
|
+
<li>
|
|
145
|
+
<a href="/docs/api/reference">Overview</a>
|
|
146
|
+
</li>
|
|
147
|
+
<li>
|
|
148
|
+
<a href="/docs/api/reference/types">Types</a>
|
|
149
|
+
</li>
|
|
150
|
+
</ul>
|
|
151
|
+
|
|
152
|
+
<h3>Guides</h3>
|
|
153
|
+
<ul>
|
|
154
|
+
<li>
|
|
155
|
+
<a href="/docs/guides/routing">Routing</a>
|
|
156
|
+
</li>
|
|
157
|
+
<li>
|
|
158
|
+
<a href="/docs/guides/layouts">Layouts</a>
|
|
159
|
+
</li>
|
|
160
|
+
</ul>
|
|
161
|
+
</aside>
|
|
162
|
+
|
|
163
|
+
<main className="docs-main">
|
|
164
|
+
<nav className="docs-breadcrumb">
|
|
165
|
+
<a href="/">Home</a>
|
|
166
|
+
{' / '}
|
|
167
|
+
<a href="/docs/getting-started">Docs</a>
|
|
168
|
+
{segments.map((segment, i) => (
|
|
169
|
+
<span key={segment}>
|
|
170
|
+
{' / '}
|
|
171
|
+
{i === segments.length - 1 ? (
|
|
172
|
+
segment
|
|
173
|
+
) : (
|
|
174
|
+
<a href={`/docs/${segments.slice(0, i + 1).join('/')}`}>{segment}</a>
|
|
175
|
+
)}
|
|
176
|
+
</span>
|
|
177
|
+
))}
|
|
178
|
+
</nav>
|
|
179
|
+
|
|
180
|
+
{doc ? (
|
|
181
|
+
<article className="docs-content">
|
|
182
|
+
<h1>{doc.title}</h1>
|
|
183
|
+
<p>{doc.content}</p>
|
|
184
|
+
|
|
185
|
+
<div className="docs-meta">
|
|
186
|
+
<p>
|
|
187
|
+
<strong>Path:</strong> /docs/{slug}
|
|
188
|
+
</p>
|
|
189
|
+
<p>
|
|
190
|
+
<strong>Segments:</strong> {segments.length} ({segments.join(' -> ')})
|
|
191
|
+
</p>
|
|
192
|
+
<p>
|
|
193
|
+
<strong>Route Pattern:</strong> [...slug] (catch-all)
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</article>
|
|
197
|
+
) : (
|
|
198
|
+
<div className="not-found">
|
|
199
|
+
<h1>Documentation Not Found</h1>
|
|
200
|
+
<p>
|
|
201
|
+
No documentation exists for: <code>/docs/{slug}</code>
|
|
202
|
+
</p>
|
|
203
|
+
<p>
|
|
204
|
+
<a href="/docs/getting-started">Go to Getting Started</a>
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</main>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component that runs on the server at request time.
|
|
5
|
+
* For database access, use the API endpoints at /api/users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default function HomePage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="home-page">
|
|
11
|
+
<header className="hero">
|
|
12
|
+
<h1>Welcome to VeloxTS</h1>
|
|
13
|
+
<p className="tagline">Type-safe full-stack development with React Server Components</p>
|
|
14
|
+
</header>
|
|
15
|
+
|
|
16
|
+
<section className="stats">
|
|
17
|
+
<div className="stat-card">
|
|
18
|
+
<span className="stat-label">Users in Database</span>
|
|
19
|
+
<p>
|
|
20
|
+
Use the API at <code>/api/users</code> to manage users.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</section>
|
|
24
|
+
|
|
25
|
+
<section className="features">
|
|
26
|
+
<h2>Features</h2>
|
|
27
|
+
<ul>
|
|
28
|
+
<li>
|
|
29
|
+
<strong>React Server Components</strong> - Server-first rendering with streaming
|
|
30
|
+
</li>
|
|
31
|
+
<li>
|
|
32
|
+
<strong>File-Based Routing</strong> - Laravel-inspired route conventions
|
|
33
|
+
</li>
|
|
34
|
+
<li>
|
|
35
|
+
<strong>Type-Safe Actions</strong> - Server actions with Zod validation
|
|
36
|
+
</li>
|
|
37
|
+
<li>
|
|
38
|
+
<strong>Embedded API</strong> - Fastify API routes at /api/*
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<footer className="cta">
|
|
44
|
+
<p>
|
|
45
|
+
Edit <code>app/pages/index.tsx</code> to customize this page.
|
|
46
|
+
</p>
|
|
47
|
+
</footer>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print Page
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates per-route layout configuration.
|
|
5
|
+
* This page uses the minimal layout (replace mode) instead of the default root layout.
|
|
6
|
+
*
|
|
7
|
+
* In entry.server.tsx, this route is defined as:
|
|
8
|
+
* defineRoute('/print', PrintPage, [MinimalLayout])
|
|
9
|
+
*
|
|
10
|
+
* The minimal layout removes navigation, footer, and other UI elements
|
|
11
|
+
* that shouldn't appear in print output.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PageConfig, PageProps } from '@veloxts/web';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Per-route layout configuration
|
|
18
|
+
*
|
|
19
|
+
* This configuration tells the router to use MinimalLayout instead of
|
|
20
|
+
* the default RootLayout. Modes available:
|
|
21
|
+
* - 'replace': Use only the specified layouts (replaces inherited)
|
|
22
|
+
* - 'append': Add layouts after inherited layouts
|
|
23
|
+
* - 'prepend': Add layouts before inherited layouts
|
|
24
|
+
* - 'inherit': Use inherited layouts (default)
|
|
25
|
+
*/
|
|
26
|
+
export const config: PageConfig = {
|
|
27
|
+
layout: {
|
|
28
|
+
// Note: In static routing, layouts are specified in defineRoute()
|
|
29
|
+
// This config is for documentation and future dynamic routing support
|
|
30
|
+
layouts: [], // Would reference MinimalLayout
|
|
31
|
+
mode: 'replace',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function PrintPage({ searchParams }: PageProps) {
|
|
36
|
+
const title = typeof searchParams.title === 'string' ? searchParams.title : 'Document';
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="print-page">
|
|
40
|
+
<header className="print-header">
|
|
41
|
+
<h1>{title}</h1>
|
|
42
|
+
<p className="print-date">Printed: {new Date().toLocaleDateString()}</p>
|
|
43
|
+
</header>
|
|
44
|
+
|
|
45
|
+
<main className="print-content">
|
|
46
|
+
<section>
|
|
47
|
+
<h2>Per-Route Layout Configuration</h2>
|
|
48
|
+
<p>This page uses the minimal layout instead of the default root layout.</p>
|
|
49
|
+
<p>
|
|
50
|
+
The minimal layout removes navigation, footer, and styling that would interfere with
|
|
51
|
+
printing.
|
|
52
|
+
</p>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section>
|
|
56
|
+
<h2>How It Works</h2>
|
|
57
|
+
<p>In the route definition:</p>
|
|
58
|
+
<pre>
|
|
59
|
+
<code>{`defineRoute('/print', PrintPage, [MinimalLayout])`}</code>
|
|
60
|
+
</pre>
|
|
61
|
+
<p>The third parameter specifies the layout chain for this specific route.</p>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section>
|
|
65
|
+
<h2>Use Cases</h2>
|
|
66
|
+
<ul>
|
|
67
|
+
<li>Print-friendly document views</li>
|
|
68
|
+
<li>Embedded widgets or iframes</li>
|
|
69
|
+
<li>Authentication pages with different branding</li>
|
|
70
|
+
<li>Landing pages with custom layouts</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</section>
|
|
73
|
+
</main>
|
|
74
|
+
|
|
75
|
+
<footer className="print-footer">
|
|
76
|
+
<p>Generated by VeloxTS Framework</p>
|
|
77
|
+
</footer>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post Detail Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component that displays a single post.
|
|
5
|
+
* Demonstrates multi-level nested dynamic routes: /users/:id/posts/:postId
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from '../../../../../src/api/database.js';
|
|
9
|
+
|
|
10
|
+
interface PageProps {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
searchParams: Record<string, string | string[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function PostDetailPage({ params }: PageProps) {
|
|
16
|
+
const { id: userId, postId } = params;
|
|
17
|
+
|
|
18
|
+
// Fetch post with user
|
|
19
|
+
const post = await db.post.findFirst({
|
|
20
|
+
where: {
|
|
21
|
+
id: postId,
|
|
22
|
+
userId: userId,
|
|
23
|
+
},
|
|
24
|
+
include: {
|
|
25
|
+
user: {
|
|
26
|
+
select: { id: true, name: true, email: true },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!post) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="post-detail-page">
|
|
34
|
+
<h1>Post Not Found</h1>
|
|
35
|
+
<p>
|
|
36
|
+
No post exists with ID: <code>{postId}</code>
|
|
37
|
+
</p>
|
|
38
|
+
<p>
|
|
39
|
+
<a href={`/users/${userId}/posts`}>Back to Posts</a>
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="post-detail-page">
|
|
47
|
+
<article className="post">
|
|
48
|
+
<header className="post-header">
|
|
49
|
+
<h1>{post.title}</h1>
|
|
50
|
+
<p className="post-meta">
|
|
51
|
+
By <a href={`/users/${post.user.id}`}>{post.user.name}</a>
|
|
52
|
+
{' | '}
|
|
53
|
+
{post.published ? 'Published' : 'Draft'}
|
|
54
|
+
{' | '}
|
|
55
|
+
{post.createdAt.toLocaleDateString()}
|
|
56
|
+
</p>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<section className="post-content">
|
|
60
|
+
{post.content ? <p>{post.content}</p> : <p className="empty-content">No content</p>}
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<footer className="post-footer">
|
|
64
|
+
<dl>
|
|
65
|
+
<dt>Post ID</dt>
|
|
66
|
+
<dd>
|
|
67
|
+
<code>{post.id}</code>
|
|
68
|
+
</dd>
|
|
69
|
+
|
|
70
|
+
<dt>Author Email</dt>
|
|
71
|
+
<dd>{post.user.email}</dd>
|
|
72
|
+
|
|
73
|
+
<dt>Created</dt>
|
|
74
|
+
<dd>{post.createdAt.toISOString()}</dd>
|
|
75
|
+
|
|
76
|
+
<dt>Updated</dt>
|
|
77
|
+
<dd>{post.updatedAt.toISOString()}</dd>
|
|
78
|
+
</dl>
|
|
79
|
+
</footer>
|
|
80
|
+
</article>
|
|
81
|
+
|
|
82
|
+
<nav className="actions">
|
|
83
|
+
<a href={`/users/${userId}/posts`}>Back to Posts</a>
|
|
84
|
+
{' | '}
|
|
85
|
+
<a href={`/users/${userId}`}>User Profile</a>
|
|
86
|
+
</nav>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Posts List Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component that displays all posts for a specific user.
|
|
5
|
+
* Demonstrates multi-level nested dynamic routes: /users/:id/posts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from '../../../../../src/api/database.js';
|
|
9
|
+
|
|
10
|
+
interface PageProps {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
searchParams: Record<string, string | string[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function UserPostsPage({ params }: PageProps) {
|
|
16
|
+
const { id: userId } = params;
|
|
17
|
+
|
|
18
|
+
// Fetch user with posts
|
|
19
|
+
const user = await db.user.findUnique({
|
|
20
|
+
where: { id: userId },
|
|
21
|
+
include: {
|
|
22
|
+
posts: {
|
|
23
|
+
orderBy: { createdAt: 'desc' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!user) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="posts-page">
|
|
31
|
+
<h1>User Not Found</h1>
|
|
32
|
+
<p>
|
|
33
|
+
No user exists with ID: <code>{userId}</code>
|
|
34
|
+
</p>
|
|
35
|
+
<p>
|
|
36
|
+
<a href="/users">Back to Users</a>
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="posts-page">
|
|
44
|
+
<header className="page-header">
|
|
45
|
+
<h1>Posts by {user.name}</h1>
|
|
46
|
+
<p>
|
|
47
|
+
<a href={`/users/${userId}`}>Back to User Profile</a>
|
|
48
|
+
{' | '}
|
|
49
|
+
<a href={`/users/${userId}/posts/new`}>New Post</a>
|
|
50
|
+
</p>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
{user.posts.length === 0 ? (
|
|
54
|
+
<p className="empty-state">No posts yet. Create the first one!</p>
|
|
55
|
+
) : (
|
|
56
|
+
<ul className="posts-list">
|
|
57
|
+
{user.posts.map((post) => (
|
|
58
|
+
<li key={post.id} className="post-item">
|
|
59
|
+
<a href={`/users/${userId}/posts/${post.id}`}>
|
|
60
|
+
<h2>{post.title}</h2>
|
|
61
|
+
{post.content && <p>{post.content.substring(0, 100)}...</p>}
|
|
62
|
+
<small>
|
|
63
|
+
{post.published ? 'Published' : 'Draft'} - {post.createdAt.toLocaleDateString()}
|
|
64
|
+
</small>
|
|
65
|
+
</a>
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<footer className="page-footer">
|
|
72
|
+
<p>Total: {user.posts.length} posts</p>
|
|
73
|
+
</footer>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New Post Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component with a form to create a new post.
|
|
5
|
+
* Demonstrates static route precedence: /users/:id/posts/new
|
|
6
|
+
* (static 'new' matches before dynamic :postId)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { db } from '../../../../../src/api/database.js';
|
|
10
|
+
|
|
11
|
+
interface PageProps {
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
searchParams: Record<string, string | string[]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function NewPostPage({ params }: PageProps) {
|
|
17
|
+
const { id: userId } = params;
|
|
18
|
+
|
|
19
|
+
// Verify user exists
|
|
20
|
+
const user = await db.user.findUnique({
|
|
21
|
+
where: { id: userId },
|
|
22
|
+
select: { id: true, name: true },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!user) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="new-post-page">
|
|
28
|
+
<h1>User Not Found</h1>
|
|
29
|
+
<p>
|
|
30
|
+
Cannot create post - no user exists with ID: <code>{userId}</code>
|
|
31
|
+
</p>
|
|
32
|
+
<p>
|
|
33
|
+
<a href="/users">Back to Users</a>
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="new-post-page">
|
|
41
|
+
<header className="page-header">
|
|
42
|
+
<h1>New Post</h1>
|
|
43
|
+
<p>Create a new post for {user.name}</p>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<form className="post-form" action={`/api/users/${userId}/posts`} method="POST">
|
|
47
|
+
<div className="form-group">
|
|
48
|
+
<label htmlFor="title">Title</label>
|
|
49
|
+
<input type="text" id="title" name="title" required placeholder="Enter post title" />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="form-group">
|
|
53
|
+
<label htmlFor="content">Content</label>
|
|
54
|
+
<textarea id="content" name="content" rows={6} placeholder="Write your post content..." />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="form-group">
|
|
58
|
+
<label>
|
|
59
|
+
<input type="checkbox" name="published" value="true" /> Publish immediately
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="form-actions">
|
|
64
|
+
<button type="submit">Create Post</button>
|
|
65
|
+
<a href={`/users/${userId}/posts`}>Cancel</a>
|
|
66
|
+
</div>
|
|
67
|
+
</form>
|
|
68
|
+
|
|
69
|
+
<footer className="page-footer">
|
|
70
|
+
<p>
|
|
71
|
+
<small>
|
|
72
|
+
Note: This page demonstrates static route precedence. The path /users/:id/posts/new
|
|
73
|
+
matches before /users/:id/posts/:postId
|
|
74
|
+
</small>
|
|
75
|
+
</p>
|
|
76
|
+
</footer>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Detail Page
|
|
3
|
+
*
|
|
4
|
+
* A React Server Component that displays a single user.
|
|
5
|
+
* Demonstrates dynamic route parameters with [id] segment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from '../../../src/api/database.js';
|
|
9
|
+
|
|
10
|
+
interface PageProps {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
searchParams: Record<string, string | string[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function UserDetailPage({ params }: PageProps) {
|
|
16
|
+
const { id } = params;
|
|
17
|
+
|
|
18
|
+
// Fetch user from database
|
|
19
|
+
const user = await db.user.findUnique({
|
|
20
|
+
where: { id },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!user) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="user-detail-page">
|
|
26
|
+
<h1>User Not Found</h1>
|
|
27
|
+
<p>
|
|
28
|
+
No user exists with ID: <code>{id}</code>
|
|
29
|
+
</p>
|
|
30
|
+
<p>
|
|
31
|
+
<a href="/users">Back to Users</a>
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="user-detail-page">
|
|
39
|
+
<h1>{user.name}</h1>
|
|
40
|
+
|
|
41
|
+
<section className="user-info">
|
|
42
|
+
<dl>
|
|
43
|
+
<dt>Email</dt>
|
|
44
|
+
<dd>{user.email}</dd>
|
|
45
|
+
|
|
46
|
+
<dt>ID</dt>
|
|
47
|
+
<dd>
|
|
48
|
+
<code>{user.id}</code>
|
|
49
|
+
</dd>
|
|
50
|
+
|
|
51
|
+
<dt>Created</dt>
|
|
52
|
+
<dd>{user.createdAt.toISOString()}</dd>
|
|
53
|
+
|
|
54
|
+
<dt>Updated</dt>
|
|
55
|
+
<dd>{user.updatedAt.toISOString()}</dd>
|
|
56
|
+
</dl>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<footer className="actions">
|
|
60
|
+
<a href="/users">Back to Users</a>
|
|
61
|
+
</footer>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|