@nitronjs/framework 0.2.0 → 0.2.2
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 +331 -253
- package/lib/Database/Migration/MigrationRepository.js +2 -6
- package/lib/Database/Model.js +7 -8
- package/lib/Database/Seeder/SeederRepository.js +2 -6
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +2 -26
- package/skeleton/resources/views/Site/Home.tsx +63 -267
package/README.md
CHANGED
|
@@ -1,102 +1,138 @@
|
|
|
1
1
|
# NitronJS
|
|
2
2
|
|
|
3
|
-
A modern
|
|
3
|
+
A modern full-stack Node.js framework with React Server Components, Islands Architecture, and built-in tooling. Built on React and Fastify for high performance and developer productivity.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why NitronJS?
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Hot Reload** - Development mode with automatic reloading
|
|
13
|
-
- **Middleware System** - Flexible middleware pipeline with route-specific middleware
|
|
14
|
-
- **Session Management** - Built-in session support with multiple drivers
|
|
15
|
-
- **Security** - CSRF protection, authentication, and input validation
|
|
16
|
-
- **Modern Build System** - esbuild + PostCSS + Tailwind CSS integration
|
|
7
|
+
- **Zero Config** - Start building immediately. No webpack, no babel config, no boilerplate
|
|
8
|
+
- **Server Components by Default** - All components render on the server. Add `"use client"` only where needed
|
|
9
|
+
- **Islands Architecture** - Client components hydrate independently, keeping your pages fast
|
|
10
|
+
- **Instant HMR** - Changes reflect immediately without losing state
|
|
11
|
+
- **Full-Stack** - Database ORM, authentication, sessions, validation all built-in
|
|
17
12
|
|
|
18
13
|
## Installation
|
|
19
14
|
|
|
20
|
-
Create a new NitronJS project:
|
|
21
|
-
|
|
22
15
|
```bash
|
|
23
16
|
npx @nitronjs/framework my-app
|
|
24
17
|
cd my-app
|
|
18
|
+
npm run dev
|
|
25
19
|
```
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
Your app will be running at `http://localhost:3000`
|
|
28
22
|
|
|
29
|
-
##
|
|
23
|
+
## Core Concepts
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
### Server Components (Default)
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx njs migrate --seed
|
|
36
|
-
```
|
|
27
|
+
Every `.tsx` file in `resources/views/` is a Server Component by default. They run on the server, have full access to your database, file system, and never ship JavaScript to the browser.
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
```tsx
|
|
30
|
+
// resources/views/Site/Home.tsx
|
|
31
|
+
import User from '@/app/Models/User.js';
|
|
32
|
+
|
|
33
|
+
export default async function Home() {
|
|
34
|
+
const users = await User.get(); // Direct database access
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<h1>Users</h1>
|
|
39
|
+
<ul>
|
|
40
|
+
{users.map(user => (
|
|
41
|
+
<li key={user.id}>{user.name}</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
### Client Components
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
Add `"use client"` at the top of a file to make it interactive. These components hydrate on the browser and can use React hooks.
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
```tsx
|
|
54
|
+
// resources/views/Components/Counter.tsx
|
|
55
|
+
"use client";
|
|
56
|
+
import { useState } from 'react';
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
njs dev # Start development server with hot reload
|
|
52
|
-
njs build # Build views for production
|
|
53
|
-
njs start # Start production server
|
|
54
|
-
```
|
|
58
|
+
export default function Counter() {
|
|
59
|
+
const [count, setCount] = useState(0);
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
return (
|
|
62
|
+
<button onClick={() => setCount(count + 1)}>
|
|
63
|
+
Count: {count}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
Use client components inside server components:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// resources/views/Site/Home.tsx (Server Component)
|
|
73
|
+
import Counter from '../Components/Counter';
|
|
74
|
+
|
|
75
|
+
export default function Home() {
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<h1>Welcome</h1>
|
|
79
|
+
<Counter /> {/* Hydrates as an island */}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
71
83
|
```
|
|
72
84
|
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
### Layouts
|
|
86
|
+
|
|
87
|
+
Create `Layout.tsx` files to wrap pages. Layouts are discovered automatically by walking up the directory tree.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
resources/views/
|
|
91
|
+
├── Layout.tsx # Root layout (wraps everything)
|
|
92
|
+
├── Site/
|
|
93
|
+
│ └── Home.tsx # Uses root Layout
|
|
94
|
+
└── Admin/
|
|
95
|
+
├── Layout.tsx # Admin layout (nested inside root)
|
|
96
|
+
└── Dashboard.tsx # Uses both layouts
|
|
76
97
|
```
|
|
77
98
|
|
|
78
|
-
|
|
99
|
+
```tsx
|
|
100
|
+
// resources/views/Layout.tsx
|
|
101
|
+
export default function RootLayout({ children }) {
|
|
102
|
+
return (
|
|
103
|
+
<html lang="en">
|
|
104
|
+
<head>
|
|
105
|
+
<meta charSet="UTF-8" />
|
|
106
|
+
<title>My App</title>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
{children}
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
79
115
|
|
|
116
|
+
```tsx
|
|
117
|
+
// resources/views/Admin/Layout.tsx
|
|
118
|
+
export default function AdminLayout({ children }) {
|
|
119
|
+
return (
|
|
120
|
+
<div className="admin-wrapper">
|
|
121
|
+
<nav>Admin Navigation</nav>
|
|
122
|
+
<main>{children}</main>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
80
126
|
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
├── database/
|
|
91
|
-
│ ├── migrations/ # Database migrations
|
|
92
|
-
│ └── seeders/ # Database seeders
|
|
93
|
-
├── public/ # Static assets
|
|
94
|
-
├── resources/
|
|
95
|
-
│ ├── css/ # Stylesheets
|
|
96
|
-
│ └── views/ # React components (TSX)
|
|
97
|
-
├── routes/
|
|
98
|
-
│ └── web.js # Route definitions
|
|
99
|
-
└── storage/ # File storage
|
|
127
|
+
|
|
128
|
+
Disable layout for a specific page:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
export const layout = false;
|
|
132
|
+
|
|
133
|
+
export default function FullscreenPage() {
|
|
134
|
+
return <div>No layout wrapper</div>;
|
|
135
|
+
}
|
|
100
136
|
```
|
|
101
137
|
|
|
102
138
|
## Routing
|
|
@@ -106,88 +142,86 @@ Define routes in `routes/web.js`:
|
|
|
106
142
|
```javascript
|
|
107
143
|
import { Route } from '@nitronjs/framework';
|
|
108
144
|
import HomeController from '#app/Controllers/HomeController.js';
|
|
145
|
+
import UserController from '#app/Controllers/UserController.js';
|
|
109
146
|
|
|
147
|
+
// Basic routes
|
|
110
148
|
Route.get('/', HomeController.index).name('home');
|
|
111
|
-
Route.get('/about', HomeController.about);
|
|
112
|
-
Route.post('/contact', HomeController.contact);
|
|
113
|
-
```
|
|
149
|
+
Route.get('/about', HomeController.about).name('about');
|
|
114
150
|
|
|
115
|
-
|
|
151
|
+
// Route parameters
|
|
152
|
+
Route.get('/users/:id', UserController.show).name('user.show');
|
|
116
153
|
|
|
117
|
-
|
|
154
|
+
// Route groups with middleware
|
|
118
155
|
Route.prefix('/admin').middleware('auth').group(() => {
|
|
119
|
-
Route.get('/dashboard', AdminController.dashboard);
|
|
120
|
-
Route.get('/users', AdminController.users);
|
|
156
|
+
Route.get('/dashboard', AdminController.dashboard).name('admin.dashboard');
|
|
157
|
+
Route.get('/users', AdminController.users).name('admin.users');
|
|
121
158
|
});
|
|
159
|
+
|
|
160
|
+
// RESTful routes
|
|
161
|
+
Route.post('/users', UserController.store);
|
|
162
|
+
Route.put('/users/:id', UserController.update);
|
|
163
|
+
Route.delete('/users/:id', UserController.destroy);
|
|
122
164
|
```
|
|
123
165
|
|
|
124
|
-
|
|
166
|
+
Generate URLs using route names:
|
|
125
167
|
|
|
126
|
-
|
|
168
|
+
```tsx
|
|
169
|
+
<a href={route('user.show', { id: 1 })}>View User</a>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Controllers
|
|
127
173
|
|
|
128
174
|
```javascript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
static async contact(request, reply) {
|
|
137
|
-
const { name, email } = request.body;
|
|
138
|
-
// Handle contact form
|
|
139
|
-
return reply.redirect('/thank-you');
|
|
175
|
+
// app/Controllers/UserController.js
|
|
176
|
+
class UserController {
|
|
177
|
+
static async index(req, res) {
|
|
178
|
+
const users = await User.get();
|
|
179
|
+
|
|
180
|
+
return res.view('User/Index', { users });
|
|
140
181
|
}
|
|
141
|
-
}
|
|
142
182
|
|
|
143
|
-
|
|
144
|
-
|
|
183
|
+
static async show(req, res) {
|
|
184
|
+
const user = await User.find(req.params.id);
|
|
145
185
|
|
|
146
|
-
|
|
186
|
+
if (!user) {
|
|
187
|
+
return res.status(404).view('Errors/NotFound');
|
|
188
|
+
}
|
|
147
189
|
|
|
148
|
-
|
|
190
|
+
return res.view('User/Show', { user });
|
|
191
|
+
}
|
|
149
192
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<h1>{title}</h1>
|
|
161
|
-
<ul>
|
|
162
|
-
{items.map(item => (
|
|
163
|
-
<li key={item.id}>{item.name}</li>
|
|
164
|
-
))}
|
|
165
|
-
</ul>
|
|
166
|
-
</body>
|
|
167
|
-
</html>
|
|
168
|
-
);
|
|
193
|
+
static async store(req, res) {
|
|
194
|
+
const { name, email } = req.body;
|
|
195
|
+
|
|
196
|
+
const user = new User();
|
|
197
|
+
user.name = name;
|
|
198
|
+
user.email = email;
|
|
199
|
+
await user.save();
|
|
200
|
+
|
|
201
|
+
return res.redirect(route('user.show', { id: user.id }));
|
|
202
|
+
}
|
|
169
203
|
}
|
|
170
|
-
```
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
export default UserController;
|
|
206
|
+
```
|
|
173
207
|
|
|
174
|
-
##
|
|
208
|
+
## Database
|
|
175
209
|
|
|
176
|
-
|
|
210
|
+
### Models
|
|
177
211
|
|
|
178
212
|
```javascript
|
|
213
|
+
// app/Models/User.js
|
|
179
214
|
import { Model } from '@nitronjs/framework';
|
|
180
215
|
|
|
181
216
|
export default class User extends Model {
|
|
182
217
|
static table = 'users';
|
|
183
|
-
static primaryKey = 'id';
|
|
184
218
|
}
|
|
185
219
|
```
|
|
186
220
|
|
|
187
|
-
###
|
|
221
|
+
### Queries
|
|
188
222
|
|
|
189
223
|
```javascript
|
|
190
|
-
// Find all
|
|
224
|
+
// Find all
|
|
191
225
|
const users = await User.get();
|
|
192
226
|
|
|
193
227
|
// Find by ID
|
|
@@ -195,63 +229,40 @@ const user = await User.find(1);
|
|
|
195
229
|
|
|
196
230
|
// Where clauses
|
|
197
231
|
const admins = await User.where('role', '=', 'admin').get();
|
|
232
|
+
const active = await User.where('active', true).get();
|
|
198
233
|
|
|
199
|
-
//
|
|
234
|
+
// Chaining
|
|
235
|
+
const results = await User
|
|
236
|
+
.where('role', '=', 'admin')
|
|
237
|
+
.where('active', true)
|
|
238
|
+
.orderBy('created_at', 'desc')
|
|
239
|
+
.limit(10)
|
|
240
|
+
.get();
|
|
241
|
+
|
|
242
|
+
// First result
|
|
243
|
+
const user = await User.where('email', '=', 'john@example.com').first();
|
|
244
|
+
|
|
245
|
+
// Create
|
|
200
246
|
const user = new User();
|
|
201
247
|
user.name = 'John';
|
|
202
248
|
user.email = 'john@example.com';
|
|
203
249
|
await user.save();
|
|
204
250
|
|
|
205
251
|
// Update
|
|
206
|
-
await User.where('id', '=', 1).update({
|
|
207
|
-
name: 'Jane'
|
|
208
|
-
});
|
|
252
|
+
await User.where('id', '=', 1).update({ name: 'Jane' });
|
|
209
253
|
|
|
210
254
|
// Delete
|
|
211
255
|
await User.where('id', '=', 1).delete();
|
|
212
256
|
```
|
|
213
257
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Create middleware with `njs make:middleware`:
|
|
217
|
-
|
|
218
|
-
```javascript
|
|
219
|
-
class CheckAge {
|
|
220
|
-
static async handler(req, res) {
|
|
221
|
-
const age = req.query.age;
|
|
222
|
-
|
|
223
|
-
if (age < 18) {
|
|
224
|
-
return res.status(403).send('Access denied');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// If no return, continues to next middleware/handler
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export default CheckAge;
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Register middleware in `app/Kernel.js`:
|
|
235
|
-
|
|
236
|
-
```javascript
|
|
237
|
-
export default {
|
|
238
|
-
routeMiddlewares: {
|
|
239
|
-
"check-age": CheckAge,
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Apply to routes:
|
|
258
|
+
### Migrations
|
|
245
259
|
|
|
246
|
-
```
|
|
247
|
-
|
|
260
|
+
```bash
|
|
261
|
+
npm run make:migration create_users_table
|
|
248
262
|
```
|
|
249
263
|
|
|
250
|
-
## Database Migrations
|
|
251
|
-
|
|
252
|
-
Create migrations with `njs make:migration`:
|
|
253
|
-
|
|
254
264
|
```javascript
|
|
265
|
+
// database/migrations/2024_01_01_000000_create_users_table.js
|
|
255
266
|
import { Schema } from '@nitronjs/framework';
|
|
256
267
|
|
|
257
268
|
class CreateUsersTable {
|
|
@@ -265,7 +276,7 @@ class CreateUsersTable {
|
|
|
265
276
|
table.timestamp('updated_at').nullable();
|
|
266
277
|
});
|
|
267
278
|
}
|
|
268
|
-
|
|
279
|
+
|
|
269
280
|
static async down() {
|
|
270
281
|
await Schema.dropIfExists('users');
|
|
271
282
|
}
|
|
@@ -274,84 +285,67 @@ class CreateUsersTable {
|
|
|
274
285
|
export default CreateUsersTable;
|
|
275
286
|
```
|
|
276
287
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
All configuration files are in the `config/` directory:
|
|
280
|
-
|
|
281
|
-
- **app.js** - Application settings (name, env, debug mode)
|
|
282
|
-
- **database.js** - Database connections
|
|
283
|
-
- **server.js** - Server configuration (port, host, CORS)
|
|
284
|
-
- **session.js** - Session management
|
|
285
|
-
- **auth.js** - Authentication settings
|
|
286
|
-
- **hash.js** - Hashing algorithm configuration
|
|
288
|
+
Run migrations:
|
|
287
289
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const appName = Config.get('app.name');
|
|
294
|
-
const dbHost = Config.get('database.host');
|
|
290
|
+
```bash
|
|
291
|
+
npm run migrate # Run pending migrations
|
|
292
|
+
npm run migrate:fresh # Drop all tables and re-run
|
|
293
|
+
npm run migrate:fresh --seed # Drop, migrate, and seed
|
|
295
294
|
```
|
|
296
295
|
|
|
297
|
-
##
|
|
296
|
+
## Authentication
|
|
298
297
|
|
|
299
298
|
```javascript
|
|
300
|
-
//
|
|
301
|
-
|
|
299
|
+
// In a controller or middleware
|
|
300
|
+
class AuthController {
|
|
301
|
+
static async login(req, res) {
|
|
302
|
+
const { email, password } = req.body;
|
|
302
303
|
|
|
303
|
-
|
|
304
|
-
const user = req.session.get('user');
|
|
304
|
+
const success = await req.auth.attempt({ email, password });
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Remove session data
|
|
312
|
-
req.session.set('user', null);
|
|
306
|
+
if (success) {
|
|
307
|
+
return req.redirect('/dashboard');
|
|
308
|
+
}
|
|
313
309
|
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
return req.view('Auth/Login', {
|
|
311
|
+
error: 'Invalid credentials'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
316
314
|
|
|
317
|
-
|
|
318
|
-
req.
|
|
315
|
+
static async logout(req, res) {
|
|
316
|
+
await req.auth.logout();
|
|
319
317
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
return res.redirect('/');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
323
321
|
|
|
324
|
-
//
|
|
325
|
-
|
|
322
|
+
// Check authentication
|
|
323
|
+
if (req.auth.check()) {
|
|
324
|
+
const user = await req.auth.user();
|
|
325
|
+
}
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
-
##
|
|
329
|
-
|
|
330
|
-
Built-in authentication system (request-based):
|
|
328
|
+
## Session
|
|
331
329
|
|
|
332
330
|
```javascript
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
email: 'admin@example.com',
|
|
336
|
-
password: 'secret'
|
|
337
|
-
});
|
|
338
|
-
// veya belirli bir guard ile:
|
|
339
|
-
// const success = await req.auth.guard('user').attempt({ email, password });
|
|
331
|
+
// Set value
|
|
332
|
+
req.session.set('key', 'value');
|
|
340
333
|
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
// veya
|
|
344
|
-
// const user = await req.auth.guard('user').user();
|
|
334
|
+
// Get value
|
|
335
|
+
const value = req.session.get('key');
|
|
345
336
|
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
337
|
+
// Flash messages (one-time)
|
|
338
|
+
req.session.flash('success', 'Profile updated!');
|
|
339
|
+
const message = req.session.getFlash('success');
|
|
340
|
+
|
|
341
|
+
// All data
|
|
342
|
+
const data = req.session.all();
|
|
343
|
+
|
|
344
|
+
// Regenerate session ID (after login)
|
|
345
|
+
req.session.regenerate();
|
|
350
346
|
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
// veya
|
|
354
|
-
// await req.auth.guard('user').logout();
|
|
347
|
+
// CSRF token
|
|
348
|
+
const token = req.session.getCsrfToken();
|
|
355
349
|
```
|
|
356
350
|
|
|
357
351
|
## Validation
|
|
@@ -360,9 +354,10 @@ await req.auth.logout();
|
|
|
360
354
|
import { Validator } from '@nitronjs/framework';
|
|
361
355
|
|
|
362
356
|
const validation = Validator.make(req.body, {
|
|
357
|
+
name: 'required|string|min:2|max:100',
|
|
363
358
|
email: 'required|email',
|
|
364
359
|
password: 'required|string|min:8',
|
|
365
|
-
age: '
|
|
360
|
+
age: 'numeric|min:18'
|
|
366
361
|
});
|
|
367
362
|
|
|
368
363
|
if (validation.fails()) {
|
|
@@ -372,58 +367,141 @@ if (validation.fails()) {
|
|
|
372
367
|
}
|
|
373
368
|
```
|
|
374
369
|
|
|
375
|
-
##
|
|
370
|
+
## Middleware
|
|
376
371
|
|
|
377
|
-
Create
|
|
372
|
+
Create custom middleware:
|
|
378
373
|
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
374
|
+
```javascript
|
|
375
|
+
// app/Middlewares/CheckAge.js
|
|
376
|
+
class CheckAge {
|
|
377
|
+
static async handler(req, res) {
|
|
378
|
+
if (req.query.age < 18) {
|
|
379
|
+
return res.status(403).send('Access denied');
|
|
380
|
+
}
|
|
381
|
+
// No return = continue to next handler
|
|
382
|
+
}
|
|
383
|
+
}
|
|
385
384
|
|
|
386
|
-
|
|
387
|
-
DATABASE_HOST=127.0.0.1
|
|
388
|
-
DATABASE_PORT=3306
|
|
389
|
-
DATABASE_NAME=nitronjs
|
|
390
|
-
DATABASE_USERNAME=root
|
|
391
|
-
DATABASE_PASSWORD=
|
|
385
|
+
export default CheckAge;
|
|
392
386
|
```
|
|
393
387
|
|
|
394
|
-
|
|
388
|
+
Register in `app/Kernel.js`:
|
|
395
389
|
|
|
396
390
|
```javascript
|
|
397
|
-
|
|
398
|
-
|
|
391
|
+
export default {
|
|
392
|
+
routeMiddlewares: {
|
|
393
|
+
'check-age': CheckAge,
|
|
394
|
+
'auth': AuthMiddleware,
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
```
|
|
399
398
|
|
|
400
|
-
|
|
401
|
-
|
|
399
|
+
Apply to routes:
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
Route.middleware('check-age').get('/restricted', Controller.method);
|
|
403
|
+
Route.middleware('auth', 'check-age').get('/admin', AdminController.index);
|
|
404
|
+
```
|
|
402
405
|
|
|
403
|
-
|
|
404
|
-
return res.redirect('/dashboard');
|
|
406
|
+
## CSS & Tailwind
|
|
405
407
|
|
|
406
|
-
|
|
407
|
-
return res.status(404).send('Not Found');
|
|
408
|
+
Put your CSS files in `resources/css/`. Tailwind CSS is automatically detected and processed.
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
```css
|
|
411
|
+
/* resources/css/app.css */
|
|
412
|
+
@import "tailwindcss";
|
|
411
413
|
```
|
|
412
414
|
|
|
413
|
-
|
|
415
|
+
CSS files are automatically linked in your views:
|
|
414
416
|
|
|
415
|
-
|
|
417
|
+
```tsx
|
|
418
|
+
// CSS from resources/css/ is available at /storage/css/
|
|
419
|
+
<link rel="stylesheet" href="/storage/css/app.css" />
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## CLI Commands
|
|
416
423
|
|
|
417
424
|
```bash
|
|
418
|
-
|
|
419
|
-
npm run
|
|
425
|
+
# Development
|
|
426
|
+
npm run dev # Start dev server with HMR
|
|
427
|
+
npm run build # Build for production
|
|
428
|
+
npm run start # Start production server
|
|
429
|
+
|
|
430
|
+
# Database
|
|
431
|
+
npm run migrate # Run migrations
|
|
432
|
+
npm run migrate:fresh # Fresh migration
|
|
433
|
+
npm run seed # Run seeders
|
|
434
|
+
|
|
435
|
+
# Code Generation
|
|
436
|
+
npm run make:controller <name>
|
|
437
|
+
npm run make:model <name>
|
|
438
|
+
npm run make:middleware <name>
|
|
439
|
+
npm run make:migration <name>
|
|
440
|
+
npm run make:seeder <name>
|
|
441
|
+
|
|
442
|
+
# Utilities
|
|
443
|
+
npm run storage:link # Create storage symlink
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Project Structure
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
my-app/
|
|
450
|
+
├── app/
|
|
451
|
+
│ ├── Controllers/ # Request handlers
|
|
452
|
+
│ ├── Middlewares/ # Custom middleware
|
|
453
|
+
│ └── Models/ # Database models
|
|
454
|
+
├── config/ # Configuration files
|
|
455
|
+
│ ├── app.js
|
|
456
|
+
│ ├── database.js
|
|
457
|
+
│ └── server.js
|
|
458
|
+
├── database/
|
|
459
|
+
│ ├── migrations/ # Database migrations
|
|
460
|
+
│ └── seeders/ # Database seeders
|
|
461
|
+
├── public/ # Static assets
|
|
462
|
+
├── resources/
|
|
463
|
+
│ ├── css/ # Stylesheets
|
|
464
|
+
│ └── views/ # React components (TSX)
|
|
465
|
+
├── routes/
|
|
466
|
+
│ └── web.js # Route definitions
|
|
467
|
+
├── storage/ # File storage
|
|
468
|
+
└── .env # Environment variables
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Configuration
|
|
472
|
+
|
|
473
|
+
Access configuration values:
|
|
474
|
+
|
|
475
|
+
```javascript
|
|
476
|
+
import { Config } from '@nitronjs/framework';
|
|
477
|
+
|
|
478
|
+
const appName = Config.get('app.name');
|
|
479
|
+
const dbHost = Config.get('database.host', 'localhost'); // with default
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Environment variables in `.env`:
|
|
483
|
+
|
|
484
|
+
```env
|
|
485
|
+
APP_NAME=MyApp
|
|
486
|
+
APP_DEV=true
|
|
487
|
+
APP_PORT=3000
|
|
488
|
+
|
|
489
|
+
DATABASE_HOST=127.0.0.1
|
|
490
|
+
DATABASE_PORT=3306
|
|
491
|
+
DATABASE_NAME=myapp
|
|
492
|
+
DATABASE_USERNAME=root
|
|
493
|
+
DATABASE_PASSWORD=
|
|
420
494
|
```
|
|
421
495
|
|
|
422
496
|
## Requirements
|
|
423
497
|
|
|
424
|
-
- Node.js 18
|
|
498
|
+
- Node.js 18+
|
|
425
499
|
- MySQL 5.7+ or MariaDB 10.3+
|
|
426
500
|
|
|
501
|
+
## Documentation
|
|
502
|
+
|
|
503
|
+
Full documentation available at [nitronjs.dev](https://nitronjs.dev/docs)
|
|
504
|
+
|
|
427
505
|
## License
|
|
428
506
|
|
|
429
507
|
ISC
|
|
@@ -5,12 +5,8 @@ class MigrationRepository {
|
|
|
5
5
|
static table = 'migrations';
|
|
6
6
|
|
|
7
7
|
static async tableExists() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return true;
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
8
|
+
const [rows] = await DB.rawQuery(`SHOW TABLES LIKE '${this.table}'`);
|
|
9
|
+
return rows.length > 0;
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
static async getExecuted() {
|
package/lib/Database/Model.js
CHANGED
|
@@ -2,7 +2,6 @@ import DB from './DB.js';
|
|
|
2
2
|
|
|
3
3
|
class Model {
|
|
4
4
|
static table = null;
|
|
5
|
-
static primaryKey = 'id';
|
|
6
5
|
|
|
7
6
|
constructor(attrs = {}) {
|
|
8
7
|
Object.defineProperty(this, '_attributes', { value: {}, writable: true });
|
|
@@ -63,7 +62,7 @@ class Model {
|
|
|
63
62
|
throw new Error(`Model ${this.name} must define a static 'table' property`);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
const row = await DB.table(this.table).where(
|
|
65
|
+
const row = await DB.table(this.table).where("id", id).first();
|
|
67
66
|
|
|
68
67
|
if (!row) return null;
|
|
69
68
|
|
|
@@ -112,15 +111,15 @@ class Model {
|
|
|
112
111
|
const data = {};
|
|
113
112
|
|
|
114
113
|
for (const [key, value] of Object.entries(this._attributes)) {
|
|
115
|
-
if (value !== undefined && (key !==
|
|
114
|
+
if (value !== undefined && (key !== "id" || !this._exists)) {
|
|
116
115
|
data[key] = value;
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
if (this._exists) {
|
|
121
|
-
const primaryKeyValue = this._attributes[
|
|
120
|
+
const primaryKeyValue = this._attributes["id"];
|
|
122
121
|
await DB.table(constructor.table)
|
|
123
|
-
.where(
|
|
122
|
+
.where("id", primaryKeyValue)
|
|
124
123
|
.update(data);
|
|
125
124
|
|
|
126
125
|
Object.assign(this._attributes, data);
|
|
@@ -128,7 +127,7 @@ class Model {
|
|
|
128
127
|
}
|
|
129
128
|
else {
|
|
130
129
|
const id = await DB.table(constructor.table).insert(data);
|
|
131
|
-
this._attributes[
|
|
130
|
+
this._attributes["id"] = id;
|
|
132
131
|
|
|
133
132
|
this._original = { ...this._attributes };
|
|
134
133
|
this._exists = true;
|
|
@@ -139,14 +138,14 @@ class Model {
|
|
|
139
138
|
|
|
140
139
|
async delete() {
|
|
141
140
|
const constructor = this.constructor;
|
|
142
|
-
const primaryKeyValue = this._attributes[
|
|
141
|
+
const primaryKeyValue = this._attributes["id"];
|
|
143
142
|
|
|
144
143
|
if (!this._exists) {
|
|
145
144
|
throw new Error('Cannot delete a model that does not exist');
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
await DB.table(constructor.table)
|
|
149
|
-
.where(
|
|
148
|
+
.where("id", primaryKeyValue)
|
|
150
149
|
.delete();
|
|
151
150
|
|
|
152
151
|
this._exists = false;
|
|
@@ -5,12 +5,8 @@ class SeederRepository {
|
|
|
5
5
|
static table = 'seeders';
|
|
6
6
|
|
|
7
7
|
static async tableExists() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return true;
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
8
|
+
const [rows] = await DB.rawQuery(`SHOW TABLES LIKE '${this.table}'`);
|
|
9
|
+
return rows.length > 0;
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
static async getExecuted() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"njs": "./cli/njs.js"
|
|
@@ -1,31 +1,7 @@
|
|
|
1
1
|
class HomeController {
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
static async index(req, res) {
|
|
4
|
-
return res.view("Site/Home"
|
|
5
|
-
title: "Welcome to NitronJS",
|
|
6
|
-
features: [
|
|
7
|
-
{
|
|
8
|
-
icon: "⚡",
|
|
9
|
-
title: "Lightning Fast",
|
|
10
|
-
description: "Server-side rendering with React for blazing fast page loads"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
icon: "🔥",
|
|
14
|
-
title: "Hot Module Reload",
|
|
15
|
-
description: "See your changes instantly without losing application state"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
icon: "🛡️",
|
|
19
|
-
title: "Secure by Default",
|
|
20
|
-
description: "Built-in CSRF protection, secure sessions, and CSP headers"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
icon: "📦",
|
|
24
|
-
title: "MVC Architecture",
|
|
25
|
-
description: "Clean separation of concerns with Models, Views, and Controllers"
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
});
|
|
4
|
+
return res.view("Site/Home");
|
|
29
5
|
}
|
|
30
6
|
|
|
31
7
|
}
|
|
@@ -1,290 +1,86 @@
|
|
|
1
|
-
type Feature = {
|
|
2
|
-
icon: string;
|
|
3
|
-
title: string;
|
|
4
|
-
description: string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
1
|
type HomeProps = {
|
|
8
|
-
|
|
9
|
-
features?: Feature[];
|
|
2
|
+
version?: string;
|
|
10
3
|
};
|
|
11
4
|
|
|
12
|
-
export default function Home({
|
|
5
|
+
export default function Home({ version = "0.2.0" }: HomeProps) {
|
|
13
6
|
return (
|
|
14
7
|
<main style={{
|
|
15
8
|
minHeight: "100vh",
|
|
16
|
-
background: "
|
|
17
|
-
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
9
|
+
background: "#0a0a0a",
|
|
10
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
18
11
|
color: "#e4e4e7",
|
|
19
|
-
|
|
12
|
+
display: "flex",
|
|
13
|
+
flexDirection: "column",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
justifyContent: "center",
|
|
16
|
+
padding: "2rem",
|
|
17
|
+
boxSizing: "border-box"
|
|
20
18
|
}}>
|
|
21
|
-
{/* Background decoration */}
|
|
22
19
|
<div style={{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
bottom: 0,
|
|
28
|
-
background: "radial-gradient(circle at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%), radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%)",
|
|
29
|
-
pointerEvents: "none"
|
|
30
|
-
}} />
|
|
31
|
-
|
|
32
|
-
{/* Hero Section */}
|
|
33
|
-
<section style={{
|
|
34
|
-
minHeight: "100vh",
|
|
20
|
+
width: 64,
|
|
21
|
+
height: 64,
|
|
22
|
+
background: "#fff",
|
|
23
|
+
borderRadius: 14,
|
|
35
24
|
display: "flex",
|
|
36
|
-
flexDirection: "column",
|
|
37
25
|
alignItems: "center",
|
|
38
26
|
justifyContent: "center",
|
|
39
|
-
|
|
40
|
-
position: "relative",
|
|
41
|
-
textAlign: "center"
|
|
27
|
+
marginBottom: "2rem"
|
|
42
28
|
}}>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
|
|
48
|
-
borderRadius: 20,
|
|
49
|
-
display: "flex",
|
|
50
|
-
alignItems: "center",
|
|
51
|
-
justifyContent: "center",
|
|
52
|
-
marginBottom: "2rem",
|
|
53
|
-
boxShadow: "0 20px 60px rgba(99, 102, 241, 0.4)"
|
|
54
|
-
}}>
|
|
55
|
-
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2">
|
|
56
|
-
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
57
|
-
</svg>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
{/* Title */}
|
|
61
|
-
<h1 style={{
|
|
62
|
-
fontSize: "clamp(2.5rem, 8vw, 4.5rem)",
|
|
63
|
-
fontWeight: 800,
|
|
64
|
-
margin: 0,
|
|
65
|
-
background: "linear-gradient(135deg, #fff 0%, #a1a1aa 100%)",
|
|
66
|
-
WebkitBackgroundClip: "text",
|
|
67
|
-
WebkitTextFillColor: "transparent",
|
|
68
|
-
letterSpacing: "-0.02em",
|
|
69
|
-
lineHeight: 1.1
|
|
70
|
-
}}>
|
|
71
|
-
{title}
|
|
72
|
-
</h1>
|
|
29
|
+
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#0a0a0a" strokeWidth="2.5">
|
|
30
|
+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
31
|
+
</svg>
|
|
32
|
+
</div>
|
|
73
33
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
MVC architecture, and developer-friendly tooling.
|
|
84
|
-
</p>
|
|
34
|
+
<h1 style={{
|
|
35
|
+
fontSize: "2.5rem",
|
|
36
|
+
fontWeight: 700,
|
|
37
|
+
margin: 0,
|
|
38
|
+
color: "#fff",
|
|
39
|
+
letterSpacing: "-0.02em"
|
|
40
|
+
}}>
|
|
41
|
+
Welcome to NitronJS
|
|
42
|
+
</h1>
|
|
85
43
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
gap: "0.5rem",
|
|
97
|
-
padding: "0.875rem 1.75rem",
|
|
98
|
-
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
|
|
99
|
-
color: "#fff",
|
|
100
|
-
textDecoration: "none",
|
|
101
|
-
fontWeight: 600,
|
|
102
|
-
fontSize: "0.95rem",
|
|
103
|
-
borderRadius: 12,
|
|
104
|
-
boxShadow: "0 4px 20px rgba(99, 102, 241, 0.4)",
|
|
105
|
-
transition: "transform 0.2s, box-shadow 0.2s"
|
|
106
|
-
}}>
|
|
107
|
-
Get Started
|
|
108
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
109
|
-
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
110
|
-
</svg>
|
|
111
|
-
</a>
|
|
112
|
-
<a href="https://github.com/nicatdursunlu/nitronjs" style={{
|
|
113
|
-
display: "inline-flex",
|
|
114
|
-
alignItems: "center",
|
|
115
|
-
gap: "0.5rem",
|
|
116
|
-
padding: "0.875rem 1.75rem",
|
|
117
|
-
background: "rgba(255, 255, 255, 0.05)",
|
|
118
|
-
color: "#e4e4e7",
|
|
119
|
-
textDecoration: "none",
|
|
120
|
-
fontWeight: 600,
|
|
121
|
-
fontSize: "0.95rem",
|
|
122
|
-
borderRadius: 12,
|
|
123
|
-
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
124
|
-
transition: "background 0.2s, border-color 0.2s"
|
|
125
|
-
}}>
|
|
126
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
127
|
-
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
|
|
128
|
-
</svg>
|
|
129
|
-
GitHub
|
|
130
|
-
</a>
|
|
131
|
-
</div>
|
|
44
|
+
<p style={{
|
|
45
|
+
fontSize: "1.1rem",
|
|
46
|
+
color: "#71717a",
|
|
47
|
+
maxWidth: 400,
|
|
48
|
+
margin: "1.5rem 0 2rem",
|
|
49
|
+
lineHeight: 1.6,
|
|
50
|
+
textAlign: "center"
|
|
51
|
+
}}>
|
|
52
|
+
Your application is ready. Start building by editing the files in <code style={{ color: "#a1a1aa", background: "#1a1a1a", padding: "2px 6px", borderRadius: 4 }}>resources/views</code>
|
|
53
|
+
</p>
|
|
132
54
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
display: "flex",
|
|
138
|
-
flexDirection: "column",
|
|
55
|
+
<a
|
|
56
|
+
href="https://nitronjs.dev/docs"
|
|
57
|
+
style={{
|
|
58
|
+
display: "inline-flex",
|
|
139
59
|
alignItems: "center",
|
|
140
60
|
gap: "0.5rem",
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
margin: "0 auto"
|
|
156
|
-
}}>
|
|
157
|
-
<h2 style={{
|
|
158
|
-
fontSize: "clamp(1.75rem, 4vw, 2.5rem)",
|
|
159
|
-
fontWeight: 700,
|
|
160
|
-
textAlign: "center",
|
|
161
|
-
margin: "0 0 1rem",
|
|
162
|
-
color: "#fff"
|
|
163
|
-
}}>
|
|
164
|
-
Why NitronJS?
|
|
165
|
-
</h2>
|
|
166
|
-
<p style={{
|
|
167
|
-
textAlign: "center",
|
|
168
|
-
color: "#71717a",
|
|
169
|
-
maxWidth: 500,
|
|
170
|
-
margin: "0 auto 4rem",
|
|
171
|
-
fontSize: "1.1rem"
|
|
172
|
-
}}>
|
|
173
|
-
Everything you need to build production-ready applications
|
|
174
|
-
</p>
|
|
61
|
+
padding: "0.75rem 1.5rem",
|
|
62
|
+
background: "#fff",
|
|
63
|
+
color: "#0a0a0a",
|
|
64
|
+
textDecoration: "none",
|
|
65
|
+
fontWeight: 600,
|
|
66
|
+
fontSize: "0.9rem",
|
|
67
|
+
borderRadius: 8
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
Documentation
|
|
71
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
72
|
+
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
73
|
+
</svg>
|
|
74
|
+
</a>
|
|
175
75
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{features.map((feature, index) => (
|
|
182
|
-
<div key={index} style={{
|
|
183
|
-
padding: "2rem",
|
|
184
|
-
background: "linear-gradient(145deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))",
|
|
185
|
-
border: "1px solid rgba(255,255,255,0.06)",
|
|
186
|
-
borderRadius: 16,
|
|
187
|
-
transition: "transform 0.2s, border-color 0.2s"
|
|
188
|
-
}}>
|
|
189
|
-
<div style={{
|
|
190
|
-
width: 48,
|
|
191
|
-
height: 48,
|
|
192
|
-
background: "linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.1))",
|
|
193
|
-
borderRadius: 12,
|
|
194
|
-
display: "flex",
|
|
195
|
-
alignItems: "center",
|
|
196
|
-
justifyContent: "center",
|
|
197
|
-
fontSize: "1.5rem",
|
|
198
|
-
marginBottom: "1.25rem"
|
|
199
|
-
}}>
|
|
200
|
-
{feature.icon}
|
|
201
|
-
</div>
|
|
202
|
-
<h3 style={{
|
|
203
|
-
fontSize: "1.125rem",
|
|
204
|
-
fontWeight: 600,
|
|
205
|
-
margin: "0 0 0.75rem",
|
|
206
|
-
color: "#fff"
|
|
207
|
-
}}>
|
|
208
|
-
{feature.title}
|
|
209
|
-
</h3>
|
|
210
|
-
<p style={{
|
|
211
|
-
fontSize: "0.9rem",
|
|
212
|
-
color: "#71717a",
|
|
213
|
-
margin: 0,
|
|
214
|
-
lineHeight: 1.6
|
|
215
|
-
}}>
|
|
216
|
-
{feature.description}
|
|
217
|
-
</p>
|
|
218
|
-
</div>
|
|
219
|
-
))}
|
|
220
|
-
</div>
|
|
221
|
-
</section>
|
|
222
|
-
|
|
223
|
-
{/* Code Example Section */}
|
|
224
|
-
<section style={{
|
|
225
|
-
padding: "4rem 2rem 6rem",
|
|
226
|
-
maxWidth: 800,
|
|
227
|
-
margin: "0 auto"
|
|
228
|
-
}}>
|
|
229
|
-
<h2 style={{
|
|
230
|
-
fontSize: "clamp(1.5rem, 3vw, 2rem)",
|
|
231
|
-
fontWeight: 700,
|
|
232
|
-
textAlign: "center",
|
|
233
|
-
margin: "0 0 2rem",
|
|
234
|
-
color: "#fff"
|
|
235
|
-
}}>
|
|
236
|
-
Start Building
|
|
237
|
-
</h2>
|
|
238
|
-
<div style={{
|
|
239
|
-
background: "#0d0d1a",
|
|
240
|
-
border: "1px solid rgba(255,255,255,0.08)",
|
|
241
|
-
borderRadius: 16,
|
|
242
|
-
overflow: "hidden"
|
|
243
|
-
}}>
|
|
244
|
-
<div style={{
|
|
245
|
-
padding: "1rem 1.25rem",
|
|
246
|
-
borderBottom: "1px solid rgba(255,255,255,0.06)",
|
|
247
|
-
display: "flex",
|
|
248
|
-
alignItems: "center",
|
|
249
|
-
gap: "0.5rem"
|
|
250
|
-
}}>
|
|
251
|
-
<span style={{ width: 12, height: 12, borderRadius: "50%", background: "#ef4444" }} />
|
|
252
|
-
<span style={{ width: 12, height: 12, borderRadius: "50%", background: "#fbbf24" }} />
|
|
253
|
-
<span style={{ width: 12, height: 12, borderRadius: "50%", background: "#4ade80" }} />
|
|
254
|
-
<span style={{ marginLeft: "auto", color: "#52525b", fontSize: "0.8rem" }}>Terminal</span>
|
|
255
|
-
</div>
|
|
256
|
-
<pre style={{
|
|
257
|
-
margin: 0,
|
|
258
|
-
padding: "1.5rem",
|
|
259
|
-
fontSize: "0.9rem",
|
|
260
|
-
lineHeight: 1.8,
|
|
261
|
-
overflowX: "auto"
|
|
262
|
-
}}>
|
|
263
|
-
<code style={{ color: "#a1a1aa" }}>
|
|
264
|
-
<span style={{ color: "#71717a" }}># Create a new project</span>{"\n"}
|
|
265
|
-
<span style={{ color: "#4ade80" }}>npx</span> @nitronjs/framework my-app{"\n\n"}
|
|
266
|
-
<span style={{ color: "#71717a" }}># Start development server</span>{"\n"}
|
|
267
|
-
<span style={{ color: "#4ade80" }}>cd</span> my-app{"\n"}
|
|
268
|
-
<span style={{ color: "#4ade80" }}>npm</span> run dev
|
|
269
|
-
</code>
|
|
270
|
-
</pre>
|
|
271
|
-
</div>
|
|
272
|
-
</section>
|
|
273
|
-
|
|
274
|
-
{/* Footer */}
|
|
275
|
-
<footer style={{
|
|
276
|
-
padding: "3rem 2rem",
|
|
277
|
-
borderTop: "1px solid rgba(255,255,255,0.05)",
|
|
278
|
-
textAlign: "center"
|
|
76
|
+
<div style={{
|
|
77
|
+
position: "fixed",
|
|
78
|
+
bottom: "1.5rem",
|
|
79
|
+
color: "#3f3f46",
|
|
80
|
+
fontSize: "0.8rem"
|
|
279
81
|
}}>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
color: "#52525b",
|
|
283
|
-
fontSize: "0.875rem"
|
|
284
|
-
}}>
|
|
285
|
-
Built with ⚡ NitronJS — The Modern Full-Stack Framework
|
|
286
|
-
</p>
|
|
287
|
-
</footer>
|
|
82
|
+
v{version}
|
|
83
|
+
</div>
|
|
288
84
|
</main>
|
|
289
85
|
);
|
|
290
86
|
}
|