frontend-hamroun 1.2.64 → 1.2.65
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 +972 -216
- package/package.json +1 -1
package/README.md
CHANGED
@@ -13,36 +13,59 @@ A lightweight full-stack JavaScript framework with Virtual DOM and hooks impleme
|
|
13
13
|
|
14
14
|
## 📋 Table of Contents
|
15
15
|
|
16
|
-
- [
|
17
|
-
- [
|
18
|
-
- [
|
19
|
-
- [
|
20
|
-
- [
|
21
|
-
- [
|
22
|
-
- [
|
23
|
-
- [
|
24
|
-
- [
|
25
|
-
- [TypeScript Support](
|
26
|
-
- [Browser Compatibility](
|
27
|
-
- [
|
28
|
-
- [License](
|
29
|
-
|
30
|
-
## 🚀
|
16
|
+
- [Introduction](#-introduction)
|
17
|
+
- [Installation](#-installation)
|
18
|
+
- [Quick Start](#-quick-start)
|
19
|
+
- [Core Concepts](#-core-concepts)
|
20
|
+
- [Frontend Features](#-frontend-features)
|
21
|
+
- [Backend Features](#-backend-features)
|
22
|
+
- [API Reference](#-api-reference)
|
23
|
+
- [CLI Tools](#-cli-tools)
|
24
|
+
- [Advanced Usage](#-advanced-usage)
|
25
|
+
- [TypeScript Support](#-typescript-support)
|
26
|
+
- [Browser Compatibility](#-browser-compatibility)
|
27
|
+
- [FAQ](#-frequently-asked-questions)
|
28
|
+
- [License](#-license)
|
29
|
+
|
30
|
+
## 🚀 Introduction
|
31
|
+
|
32
|
+
Frontend Hamroun is a lightweight (~5KB gzipped) JavaScript framework designed for building modern web applications. It combines React-like frontend development with powerful backend capabilities in a single, unified package.
|
33
|
+
|
34
|
+
### Key Features
|
35
|
+
|
36
|
+
- **Efficient Virtual DOM**: Minimizes DOM operations with intelligent diffing
|
37
|
+
- **Complete Hooks API**: useState, useEffect, useRef, useMemo, and more
|
38
|
+
- **Full-Stack Solution**: Unified frontend and backend development
|
39
|
+
- **Server-Side Rendering**: Optimized SSR with hydration
|
40
|
+
- **File-Based Routing**: Intuitive API endpoint creation
|
41
|
+
- **Database Integration**: Built-in support for MongoDB, MySQL, PostgreSQL
|
42
|
+
- **TypeScript Support**: Full type definitions and excellent DX
|
43
|
+
- **Automatic Batch Updates**: Efficient state management
|
44
|
+
- **CLI Tooling**: Scaffolding for components, pages, and API routes
|
45
|
+
|
46
|
+
## 📦 Installation
|
31
47
|
|
32
48
|
```bash
|
49
|
+
# Using npm
|
33
50
|
npm install frontend-hamroun
|
51
|
+
|
52
|
+
# Using yarn
|
53
|
+
yarn add frontend-hamroun
|
54
|
+
|
55
|
+
# Using pnpm
|
56
|
+
pnpm add frontend-hamroun
|
34
57
|
```
|
35
58
|
|
36
59
|
## 🏁 Quick Start
|
37
60
|
|
38
61
|
<details>
|
39
|
-
<summary><b>Create a new project
|
62
|
+
<summary><b>Method 1: Create a new project with CLI</b></summary>
|
40
63
|
|
41
64
|
```bash
|
42
|
-
# Using
|
65
|
+
# Using npx
|
43
66
|
npx create-frontend-app my-app
|
44
67
|
|
45
|
-
# Or
|
68
|
+
# Or with the frontend-hamroun CLI
|
46
69
|
npx frontend-hamroun create my-app
|
47
70
|
```
|
48
71
|
|
@@ -53,135 +76,217 @@ cd my-app
|
|
53
76
|
npm install
|
54
77
|
npm run dev
|
55
78
|
```
|
79
|
+
|
80
|
+
This will scaffold a new project with all the necessary configuration.
|
56
81
|
</details>
|
57
82
|
|
58
83
|
<details>
|
59
|
-
<summary><b>Add to an existing project
|
84
|
+
<summary><b>Method 2: Add to an existing project</b></summary>
|
60
85
|
|
61
86
|
```bash
|
62
87
|
# Install the package
|
63
88
|
npm install frontend-hamroun
|
64
89
|
|
65
|
-
# Import
|
90
|
+
# Import and use in your code
|
66
91
|
import { render, useState } from 'frontend-hamroun';
|
67
92
|
```
|
68
|
-
</details>
|
69
|
-
|
70
|
-
## 💻 Basic Usage
|
71
93
|
|
72
94
|
```jsx
|
95
|
+
// Create a simple app
|
73
96
|
import { render, useState } from 'frontend-hamroun';
|
74
97
|
|
75
|
-
function
|
98
|
+
function Counter() {
|
76
99
|
const [count, setCount] = useState(0);
|
100
|
+
|
77
101
|
return (
|
78
102
|
<div>
|
79
103
|
<h1>Count: {count}</h1>
|
80
|
-
<button onClick={() => setCount(count + 1)}>
|
104
|
+
<button onClick={() => setCount(count + 1)}>
|
105
|
+
Increment
|
106
|
+
</button>
|
81
107
|
</div>
|
82
108
|
);
|
83
109
|
}
|
84
110
|
|
85
|
-
render(<
|
111
|
+
render(<Counter />, document.getElementById('root'));
|
86
112
|
```
|
113
|
+
</details>
|
87
114
|
|
88
|
-
##
|
89
|
-
|
90
|
-
<table>
|
91
|
-
<tr>
|
92
|
-
<td><b>🔍 Lightweight Core</b><br>Under 5KB gzipped</td>
|
93
|
-
<td><b>🌐 Full-Stack</b><br>Client and server in one</td>
|
94
|
-
<td><b>⚡ Virtual DOM</b><br>Efficient rendering</td>
|
95
|
-
</tr>
|
96
|
-
<tr>
|
97
|
-
<td><b>🪝 Hooks API</b><br>Complete hook system</td>
|
98
|
-
<td><b>📊 Context API</b><br>Simple state management</td>
|
99
|
-
<td><b>🖥️ SSR</b><br>Server-side rendering</td>
|
100
|
-
</tr>
|
101
|
-
<tr>
|
102
|
-
<td><b>🗄️ Database</b><br>Multiple DB support</td>
|
103
|
-
<td><b>🔐 Authentication</b><br>Built-in JWT</td>
|
104
|
-
<td><b>🧩 TypeScript</b><br>Full type definitions</td>
|
105
|
-
</tr>
|
106
|
-
</table>
|
107
|
-
|
108
|
-
## 🧩 Client-side Features
|
109
|
-
|
110
|
-
<details open>
|
111
|
-
<summary><b>Component Development</b></summary>
|
115
|
+
## 🧠 Core Concepts
|
112
116
|
|
113
|
-
|
114
|
-
import { useState, useEffect } from 'frontend-hamroun';
|
117
|
+
### Virtual DOM
|
115
118
|
|
116
|
-
|
119
|
+
Frontend Hamroun uses a lightweight Virtual DOM implementation to efficiently update the real DOM. It only applies the minimal necessary changes by:
|
120
|
+
|
121
|
+
```jsx
|
122
|
+
// Virtual DOM diffing occurs automatically
|
123
|
+
function App() {
|
117
124
|
const [count, setCount] = useState(0);
|
125
|
+
return (
|
126
|
+
<div>
|
127
|
+
<h1>Counter</h1>
|
128
|
+
<p>Count: {count}</p> {/* Only this text node updates */}
|
129
|
+
<button onClick={() => setCount(count + 1)}>Increment</button>
|
130
|
+
</div>
|
131
|
+
);
|
132
|
+
}
|
133
|
+
```
|
134
|
+
|
135
|
+
### Component Model
|
136
|
+
|
137
|
+
Components are the building blocks of your UI. Each component encapsulates its own logic and rendering:
|
138
|
+
|
139
|
+
```jsx
|
140
|
+
// Function components with hooks
|
141
|
+
function Greeting({ name }) {
|
142
|
+
// State management
|
143
|
+
const [clicked, setClicked] = useState(false);
|
118
144
|
|
145
|
+
// Side effects
|
119
146
|
useEffect(() => {
|
120
|
-
document.title = `
|
121
|
-
return () => document.title = 'App';
|
122
|
-
}, [
|
147
|
+
document.title = `Hello, ${name}`;
|
148
|
+
return () => { document.title = 'App'; };
|
149
|
+
}, [name]);
|
123
150
|
|
124
151
|
return (
|
125
|
-
<div
|
126
|
-
<
|
127
|
-
<button onClick={() =>
|
128
|
-
|
152
|
+
<div>
|
153
|
+
<h1>{clicked ? `Thanks, ${name}!` : `Hello, ${name}!`}</h1>
|
154
|
+
<button onClick={() => setClicked(true)}>
|
155
|
+
{clicked ? 'Clicked!' : 'Click me'}
|
156
|
+
</button>
|
129
157
|
</div>
|
130
158
|
);
|
131
159
|
}
|
132
160
|
```
|
133
|
-
|
161
|
+
|
162
|
+
## 🎨 Frontend Features
|
163
|
+
|
164
|
+
### Hooks System
|
134
165
|
|
135
166
|
<details>
|
136
|
-
<summary><b>
|
167
|
+
<summary><b>State Management with useState</b></summary>
|
137
168
|
|
138
|
-
### useState
|
139
169
|
```jsx
|
140
|
-
|
141
|
-
setCount(count + 1); // Direct update
|
142
|
-
setCount(prev => prev + 1); // Functional update
|
143
|
-
```
|
170
|
+
import { useState } from 'frontend-hamroun';
|
144
171
|
|
145
|
-
|
146
|
-
|
147
|
-
useEffect(() => {
|
148
|
-
// Effect logic
|
149
|
-
const subscription = api.subscribe();
|
172
|
+
function Counter() {
|
173
|
+
const [count, setCount] = useState(0);
|
150
174
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
175
|
+
function increment() {
|
176
|
+
setCount(count + 1);
|
177
|
+
}
|
178
|
+
|
179
|
+
function decrement() {
|
180
|
+
setCount(count - 1);
|
181
|
+
}
|
182
|
+
|
183
|
+
// Functional updates for derived state
|
184
|
+
function double() {
|
185
|
+
setCount(prevCount => prevCount * 2);
|
186
|
+
}
|
187
|
+
|
188
|
+
return (
|
189
|
+
<div>
|
190
|
+
<h2>Count: {count}</h2>
|
191
|
+
<button onClick={increment}>+</button>
|
192
|
+
<button onClick={decrement}>-</button>
|
193
|
+
<button onClick={double}>×2</button>
|
194
|
+
</div>
|
195
|
+
);
|
196
|
+
}
|
156
197
|
```
|
198
|
+
</details>
|
157
199
|
|
158
|
-
|
159
|
-
|
160
|
-
const expensiveValue = useMemo(() => {
|
161
|
-
return computeExpensiveValue(a, b);
|
162
|
-
}, [a, b]); // Recomputes only when a or b changes
|
163
|
-
```
|
200
|
+
<details>
|
201
|
+
<summary><b>Side Effects with useEffect</b></summary>
|
164
202
|
|
165
|
-
### useRef
|
166
203
|
```jsx
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
204
|
+
import { useState, useEffect } from 'frontend-hamroun';
|
205
|
+
|
206
|
+
function UserProfile({ userId }) {
|
207
|
+
const [user, setUser] = useState(null);
|
208
|
+
const [loading, setLoading] = useState(true);
|
209
|
+
|
210
|
+
useEffect(() => {
|
211
|
+
// Reset state when userId changes
|
212
|
+
setLoading(true);
|
213
|
+
|
214
|
+
// Fetch user data
|
215
|
+
fetch(`/api/users/${userId}`)
|
216
|
+
.then(res => res.json())
|
217
|
+
.then(data => {
|
218
|
+
setUser(data);
|
219
|
+
setLoading(false);
|
220
|
+
})
|
221
|
+
.catch(err => {
|
222
|
+
console.error(err);
|
223
|
+
setLoading(false);
|
224
|
+
});
|
225
|
+
|
226
|
+
// Cleanup function runs on component unmount or before effect re-runs
|
227
|
+
return () => {
|
228
|
+
// Cancel any pending requests or subscriptions
|
229
|
+
console.log('Cleaning up effect for userId:', userId);
|
230
|
+
};
|
231
|
+
}, [userId]); // Only re-run when userId changes
|
232
|
+
|
233
|
+
if (loading) return <div>Loading...</div>;
|
234
|
+
if (!user) return <div>User not found</div>;
|
235
|
+
|
236
|
+
return (
|
237
|
+
<div>
|
238
|
+
<h1>{user.name}</h1>
|
239
|
+
<p>Email: {user.email}</p>
|
240
|
+
</div>
|
241
|
+
);
|
242
|
+
}
|
173
243
|
```
|
244
|
+
</details>
|
245
|
+
|
246
|
+
<details>
|
247
|
+
<summary><b>Performance Optimization with useMemo and useRef</b></summary>
|
174
248
|
|
175
|
-
### useErrorBoundary
|
176
249
|
```jsx
|
177
|
-
|
250
|
+
import { useState, useMemo, useRef } from 'frontend-hamroun';
|
178
251
|
|
179
|
-
|
252
|
+
function ExpensiveCalculation({ items, filter }) {
|
253
|
+
const [selectedId, setSelectedId] = useState(null);
|
254
|
+
|
255
|
+
// Cache expensive calculation results, only recalculate when dependencies change
|
256
|
+
const filteredItems = useMemo(() => {
|
257
|
+
console.log('Filtering items...');
|
258
|
+
return items.filter(item => item.name.includes(filter));
|
259
|
+
}, [items, filter]);
|
260
|
+
|
261
|
+
// Create a persistent reference that doesn't trigger re-renders
|
262
|
+
const lastRenderTime = useRef(Date.now());
|
263
|
+
|
264
|
+
console.log(`Time since last render: ${Date.now() - lastRenderTime.current}ms`);
|
265
|
+
lastRenderTime.current = Date.now();
|
266
|
+
|
267
|
+
// DOM element references
|
268
|
+
const listRef = useRef(null);
|
269
|
+
|
270
|
+
function scrollToTop() {
|
271
|
+
listRef.current.scrollTop = 0;
|
272
|
+
}
|
273
|
+
|
180
274
|
return (
|
181
|
-
<div
|
182
|
-
<
|
183
|
-
<
|
184
|
-
|
275
|
+
<div>
|
276
|
+
<button onClick={scrollToTop}>Scroll to top</button>
|
277
|
+
<div ref={listRef} style={{ height: '200px', overflow: 'auto' }}>
|
278
|
+
{filteredItems.map(item => (
|
279
|
+
<div
|
280
|
+
key={item.id}
|
281
|
+
onClick={() => setSelectedId(item.id)}
|
282
|
+
style={{
|
283
|
+
background: item.id === selectedId ? 'lightblue' : 'white'
|
284
|
+
}}
|
285
|
+
>
|
286
|
+
{item.name}
|
287
|
+
</div>
|
288
|
+
))}
|
289
|
+
</div>
|
185
290
|
</div>
|
186
291
|
);
|
187
292
|
}
|
@@ -189,55 +294,176 @@ if (error) {
|
|
189
294
|
</details>
|
190
295
|
|
191
296
|
<details>
|
192
|
-
<summary><b>Context API</b></summary>
|
297
|
+
<summary><b>Context API for State Management</b></summary>
|
193
298
|
|
194
299
|
```jsx
|
195
|
-
|
300
|
+
import { createContext, useContext, useState } from 'frontend-hamroun';
|
301
|
+
|
302
|
+
// Create a context with default value
|
196
303
|
const ThemeContext = createContext('light');
|
197
304
|
|
198
|
-
// Provider
|
199
|
-
function
|
305
|
+
// Provider component to supply context value
|
306
|
+
function ThemeProvider({ children }) {
|
200
307
|
const [theme, setTheme] = useState('light');
|
201
308
|
|
309
|
+
const toggleTheme = () => {
|
310
|
+
setTheme(theme === 'light' ? 'dark' : 'light');
|
311
|
+
};
|
312
|
+
|
202
313
|
return (
|
203
|
-
<ThemeContext.Provider value={theme}>
|
204
|
-
|
205
|
-
<Main />
|
206
|
-
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
207
|
-
Toggle theme
|
208
|
-
</button>
|
314
|
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
315
|
+
{children}
|
209
316
|
</ThemeContext.Provider>
|
210
317
|
);
|
211
318
|
}
|
212
319
|
|
213
|
-
// Consumer
|
214
|
-
function
|
215
|
-
const theme = useContext(ThemeContext);
|
216
|
-
|
320
|
+
// Consumer component using the context value
|
321
|
+
function ThemedButton() {
|
322
|
+
const { theme, toggleTheme } = useContext(ThemeContext);
|
323
|
+
|
324
|
+
return (
|
325
|
+
<button
|
326
|
+
onClick={toggleTheme}
|
327
|
+
style={{
|
328
|
+
background: theme === 'light' ? '#fff' : '#333',
|
329
|
+
color: theme === 'light' ? '#333' : '#fff',
|
330
|
+
border: '1px solid #ccc',
|
331
|
+
padding: '8px 16px',
|
332
|
+
}}
|
333
|
+
>
|
334
|
+
Switch to {theme === 'light' ? 'dark' : 'light'} mode
|
335
|
+
</button>
|
336
|
+
);
|
337
|
+
}
|
338
|
+
|
339
|
+
// Usage in application
|
340
|
+
function App() {
|
341
|
+
return (
|
342
|
+
<ThemeProvider>
|
343
|
+
<div>
|
344
|
+
<h1>Themed Application</h1>
|
345
|
+
<ThemedButton />
|
346
|
+
</div>
|
347
|
+
</ThemeProvider>
|
348
|
+
);
|
349
|
+
}
|
350
|
+
```
|
351
|
+
</details>
|
352
|
+
|
353
|
+
<details>
|
354
|
+
<summary><b>Error Boundaries with useErrorBoundary</b></summary>
|
355
|
+
|
356
|
+
```jsx
|
357
|
+
import { useErrorBoundary } from 'frontend-hamroun';
|
358
|
+
|
359
|
+
function ErrorBoundary({ children }) {
|
360
|
+
const [error, resetError] = useErrorBoundary();
|
361
|
+
|
362
|
+
if (error) {
|
363
|
+
return (
|
364
|
+
<div className="error-boundary">
|
365
|
+
<h2>Something went wrong</h2>
|
366
|
+
<p>{error.message}</p>
|
367
|
+
<button onClick={resetError}>Try again</button>
|
368
|
+
</div>
|
369
|
+
);
|
370
|
+
}
|
371
|
+
|
372
|
+
return children;
|
373
|
+
}
|
374
|
+
|
375
|
+
// Usage
|
376
|
+
function App() {
|
377
|
+
return (
|
378
|
+
<ErrorBoundary>
|
379
|
+
<UserProfile userId="123" />
|
380
|
+
</ErrorBoundary>
|
381
|
+
);
|
217
382
|
}
|
218
383
|
```
|
219
384
|
</details>
|
220
385
|
|
221
|
-
|
386
|
+
### Batch Updates for Efficiency
|
387
|
+
|
388
|
+
Frontend Hamroun automatically batches state updates within event handlers and can manually batch other updates with `batchUpdates`:
|
389
|
+
|
390
|
+
```jsx
|
391
|
+
import { useState, batchUpdates } from 'frontend-hamroun';
|
392
|
+
|
393
|
+
function Form() {
|
394
|
+
const [name, setName] = useState('');
|
395
|
+
const [email, setEmail] = useState('');
|
396
|
+
const [isSubmitting, setSubmitting] = useState(false);
|
397
|
+
const [errors, setErrors] = useState({});
|
398
|
+
|
399
|
+
async function handleSubmit(e) {
|
400
|
+
e.preventDefault();
|
401
|
+
|
402
|
+
// Group multiple state updates into a single render
|
403
|
+
batchUpdates(() => {
|
404
|
+
setSubmitting(true);
|
405
|
+
setErrors({});
|
406
|
+
});
|
407
|
+
|
408
|
+
try {
|
409
|
+
const response = await fetch('/api/users', {
|
410
|
+
method: 'POST',
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
412
|
+
body: JSON.stringify({ name, email })
|
413
|
+
});
|
414
|
+
|
415
|
+
const result = await response.json();
|
416
|
+
|
417
|
+
batchUpdates(() => {
|
418
|
+
setSubmitting(false);
|
419
|
+
setName('');
|
420
|
+
setEmail('');
|
421
|
+
});
|
422
|
+
} catch (error) {
|
423
|
+
batchUpdates(() => {
|
424
|
+
setSubmitting(false);
|
425
|
+
setErrors({ submit: error.message });
|
426
|
+
});
|
427
|
+
}
|
428
|
+
}
|
429
|
+
|
430
|
+
return (
|
431
|
+
<form onSubmit={handleSubmit}>
|
432
|
+
{/* Form fields */}
|
433
|
+
<button type="submit" disabled={isSubmitting}>
|
434
|
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
435
|
+
</button>
|
436
|
+
</form>
|
437
|
+
);
|
438
|
+
}
|
439
|
+
```
|
440
|
+
|
441
|
+
## 🖥️ Backend Features
|
442
|
+
|
443
|
+
### Express Server Integration
|
222
444
|
|
223
445
|
<details>
|
224
|
-
<summary><b>
|
446
|
+
<summary><b>Server Setup</b></summary>
|
225
447
|
|
226
448
|
```js
|
227
449
|
import { server } from 'frontend-hamroun/server';
|
228
450
|
|
229
451
|
const app = await server.createServer({
|
230
452
|
port: 3000,
|
231
|
-
apiDir: './api',
|
232
|
-
pagesDir: './pages',
|
233
|
-
staticDir: './public',
|
453
|
+
apiDir: './api', // Directory for API routes
|
454
|
+
pagesDir: './pages', // Directory for page components
|
455
|
+
staticDir: './public', // Directory for static files
|
456
|
+
|
457
|
+
// Database configuration
|
234
458
|
db: {
|
235
459
|
url: process.env.DATABASE_URL,
|
236
|
-
type: 'mongodb'
|
460
|
+
type: 'mongodb' // mongodb, mysql, or postgres
|
237
461
|
},
|
462
|
+
|
463
|
+
// Authentication configuration
|
238
464
|
auth: {
|
239
465
|
secret: process.env.JWT_SECRET,
|
240
|
-
expiresIn: '7d'
|
466
|
+
expiresIn: '7d' // Token expiration time
|
241
467
|
}
|
242
468
|
});
|
243
469
|
|
@@ -246,239 +472,769 @@ console.log('Server running at http://localhost:3000');
|
|
246
472
|
```
|
247
473
|
</details>
|
248
474
|
|
475
|
+
### File-Based API Routing
|
476
|
+
|
249
477
|
<details>
|
250
478
|
<summary><b>API Routes</b></summary>
|
251
479
|
|
252
480
|
```js
|
253
|
-
// api/users.js
|
481
|
+
// api/users.js (automatically maps to /api/users)
|
254
482
|
export async function get(req, res) {
|
483
|
+
// GET /api/users - List all users
|
255
484
|
const users = await req.db.collection('users').find().toArray();
|
256
485
|
res.json(users);
|
257
486
|
}
|
258
487
|
|
259
488
|
export async function post(req, res) {
|
489
|
+
// POST /api/users - Create a new user
|
260
490
|
const { name, email } = req.body;
|
261
491
|
|
262
492
|
if (!name || !email) {
|
263
493
|
return res.status(400).json({ error: 'Name and email are required' });
|
264
494
|
}
|
265
495
|
|
266
|
-
const result = await req.db.collection('users').insertOne({
|
267
|
-
|
496
|
+
const result = await req.db.collection('users').insertOne({
|
497
|
+
name,
|
498
|
+
email,
|
499
|
+
createdAt: new Date()
|
500
|
+
});
|
501
|
+
|
502
|
+
res.status(201).json({ id: result.insertedId });
|
268
503
|
}
|
269
504
|
```
|
270
|
-
</details>
|
271
|
-
|
272
|
-
<details>
|
273
|
-
<summary><b>Database Support (MongoDB, MySQL, PostgreSQL)</b></summary>
|
274
|
-
|
275
|
-
#### MongoDB Example
|
276
505
|
|
277
506
|
```js
|
507
|
+
// api/users/[id].js (automatically maps to /api/users/:id)
|
278
508
|
export async function get(req, res) {
|
509
|
+
// GET /api/users/:id - Get user by ID
|
510
|
+
const { id } = req.params;
|
511
|
+
|
512
|
+
try {
|
513
|
+
const user = await req.db.collection('users').findOne({
|
514
|
+
_id: new ObjectId(id)
|
515
|
+
});
|
516
|
+
|
517
|
+
if (!user) {
|
518
|
+
return res.status(404).json({ error: 'User not found' });
|
519
|
+
}
|
520
|
+
|
521
|
+
res.json(user);
|
522
|
+
} catch (error) {
|
523
|
+
res.status(500).json({ error: 'Server error' });
|
524
|
+
}
|
525
|
+
}
|
526
|
+
|
527
|
+
export async function put(req, res) {
|
528
|
+
// PUT /api/users/:id - Update user
|
529
|
+
const { id } = req.params;
|
530
|
+
const { name, email } = req.body;
|
531
|
+
|
532
|
+
await req.db.collection('users').updateOne(
|
533
|
+
{ _id: new ObjectId(id) },
|
534
|
+
{ $set: { name, email, updatedAt: new Date() } }
|
535
|
+
);
|
536
|
+
|
537
|
+
res.json({ success: true });
|
538
|
+
}
|
539
|
+
|
540
|
+
export async function del(req, res) {
|
541
|
+
// DELETE /api/users/:id - Delete user
|
542
|
+
// Note: 'delete' is a reserved word, so we use 'del'
|
279
543
|
const { id } = req.params;
|
280
|
-
|
544
|
+
|
545
|
+
await req.db.collection('users').deleteOne({
|
281
546
|
_id: new ObjectId(id)
|
282
547
|
});
|
283
548
|
|
284
|
-
|
285
|
-
|
286
|
-
|
549
|
+
res.status(204).end();
|
550
|
+
}
|
551
|
+
```
|
552
|
+
</details>
|
553
|
+
|
554
|
+
### Database Integration
|
555
|
+
|
556
|
+
<details>
|
557
|
+
<summary><b>MongoDB Example</b></summary>
|
558
|
+
|
559
|
+
```js
|
560
|
+
// api/posts.js
|
561
|
+
export async function get(req, res) {
|
562
|
+
// Complex MongoDB aggregation
|
563
|
+
const posts = await req.db.collection('posts')
|
564
|
+
.aggregate([
|
565
|
+
{ $match: { published: true } },
|
566
|
+
{ $sort: { createdAt: -1 } },
|
567
|
+
{ $limit: 10 },
|
568
|
+
{ $lookup: {
|
569
|
+
from: 'users',
|
570
|
+
localField: 'authorId',
|
571
|
+
foreignField: '_id',
|
572
|
+
as: 'author'
|
573
|
+
}},
|
574
|
+
{ $unwind: '$author' },
|
575
|
+
{ $project: {
|
576
|
+
title: 1,
|
577
|
+
content: 1,
|
578
|
+
createdAt: 1,
|
579
|
+
'author.name': 1,
|
580
|
+
'author.email': 1
|
581
|
+
}}
|
582
|
+
])
|
583
|
+
.toArray();
|
287
584
|
|
288
|
-
res.json(
|
585
|
+
res.json(posts);
|
289
586
|
}
|
290
587
|
```
|
588
|
+
</details>
|
291
589
|
|
292
|
-
|
590
|
+
<details>
|
591
|
+
<summary><b>MySQL Example</b></summary>
|
293
592
|
|
294
593
|
```js
|
594
|
+
// api/products.js
|
295
595
|
export async function get(req, res) {
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
596
|
+
// Complex SQL query with joins
|
597
|
+
const [products] = await req.db.execute(`
|
598
|
+
SELECT
|
599
|
+
p.id,
|
600
|
+
p.name,
|
601
|
+
p.price,
|
602
|
+
c.name as categoryName
|
603
|
+
FROM
|
604
|
+
products p
|
605
|
+
JOIN
|
606
|
+
categories c ON p.category_id = c.id
|
607
|
+
WHERE
|
608
|
+
p.active = ?
|
609
|
+
ORDER BY
|
610
|
+
p.created_at DESC
|
611
|
+
LIMIT 20
|
612
|
+
`, [true]);
|
613
|
+
|
614
|
+
res.json(products);
|
301
615
|
}
|
302
616
|
```
|
617
|
+
</details>
|
303
618
|
|
304
|
-
|
619
|
+
<details>
|
620
|
+
<summary><b>PostgreSQL Example</b></summary>
|
305
621
|
|
306
622
|
```js
|
623
|
+
// api/analytics.js
|
307
624
|
export async function get(req, res) {
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
625
|
+
// Advanced PostgreSQL features
|
626
|
+
const result = await req.db.query(`
|
627
|
+
WITH monthly_sales AS (
|
628
|
+
SELECT
|
629
|
+
date_trunc('month', order_date) as month,
|
630
|
+
SUM(total_amount) as revenue
|
631
|
+
FROM
|
632
|
+
orders
|
633
|
+
WHERE
|
634
|
+
order_date > NOW() - INTERVAL '1 year'
|
635
|
+
GROUP BY
|
636
|
+
date_trunc('month', order_date)
|
637
|
+
)
|
638
|
+
SELECT
|
639
|
+
month,
|
640
|
+
revenue,
|
641
|
+
lag(revenue) OVER (ORDER BY month) as prev_month_revenue,
|
642
|
+
round((revenue - lag(revenue) OVER (ORDER BY month)) /
|
643
|
+
lag(revenue) OVER (ORDER BY month) * 100, 2) as growth_percent
|
644
|
+
FROM
|
645
|
+
monthly_sales
|
646
|
+
ORDER BY
|
647
|
+
month
|
648
|
+
`);
|
649
|
+
|
312
650
|
res.json(result.rows);
|
313
651
|
}
|
314
652
|
```
|
315
653
|
</details>
|
316
654
|
|
655
|
+
### Authentication System
|
656
|
+
|
317
657
|
<details>
|
318
|
-
<summary><b>Authentication</b></summary>
|
658
|
+
<summary><b>JWT Authentication</b></summary>
|
319
659
|
|
320
660
|
```js
|
321
661
|
// api/auth/login.js
|
322
662
|
export async function post(req, res) {
|
323
|
-
const {
|
324
|
-
const user = await req.db.collection('users').findOne({ username });
|
663
|
+
const { email, password } = req.body;
|
325
664
|
|
326
|
-
|
327
|
-
|
665
|
+
// Validate input
|
666
|
+
if (!email || !password) {
|
667
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
328
668
|
}
|
329
669
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
670
|
+
try {
|
671
|
+
// Find user by email
|
672
|
+
const user = await req.db.collection('users').findOne({ email });
|
673
|
+
|
674
|
+
// Check if user exists and password is correct
|
675
|
+
if (!user || !await req.auth.comparePasswords(password, user.password)) {
|
676
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
677
|
+
}
|
678
|
+
|
679
|
+
// Generate JWT token
|
680
|
+
const token = req.auth.generateToken({
|
681
|
+
id: user._id,
|
682
|
+
name: user.name,
|
683
|
+
email: user.email,
|
684
|
+
roles: user.roles || ['user']
|
685
|
+
});
|
686
|
+
|
687
|
+
// Return token and user info (excluding sensitive data)
|
688
|
+
res.json({
|
689
|
+
token,
|
690
|
+
user: {
|
691
|
+
id: user._id,
|
692
|
+
name: user.name,
|
693
|
+
email: user.email,
|
694
|
+
roles: user.roles || ['user']
|
695
|
+
}
|
696
|
+
});
|
697
|
+
} catch (error) {
|
698
|
+
res.status(500).json({ error: 'Authentication failed' });
|
699
|
+
}
|
335
700
|
}
|
701
|
+
```
|
702
|
+
|
703
|
+
```js
|
704
|
+
// api/auth/register.js
|
705
|
+
export async function post(req, res) {
|
706
|
+
const { name, email, password } = req.body;
|
707
|
+
|
708
|
+
// Validate input
|
709
|
+
if (!name || !email || !password) {
|
710
|
+
return res.status(400).json({
|
711
|
+
error: 'Name, email, and password are required'
|
712
|
+
});
|
713
|
+
}
|
714
|
+
|
715
|
+
try {
|
716
|
+
// Check if user already exists
|
717
|
+
const existingUser = await req.db.collection('users').findOne({ email });
|
718
|
+
if (existingUser) {
|
719
|
+
return res.status(409).json({ error: 'Email already in use' });
|
720
|
+
}
|
721
|
+
|
722
|
+
// Hash password
|
723
|
+
const hashedPassword = await req.auth.hashPassword(password);
|
724
|
+
|
725
|
+
// Create user
|
726
|
+
const result = await req.db.collection('users').insertOne({
|
727
|
+
name,
|
728
|
+
email,
|
729
|
+
password: hashedPassword,
|
730
|
+
roles: ['user'],
|
731
|
+
createdAt: new Date()
|
732
|
+
});
|
733
|
+
|
734
|
+
res.status(201).json({
|
735
|
+
id: result.insertedId,
|
736
|
+
name,
|
737
|
+
email,
|
738
|
+
roles: ['user']
|
739
|
+
});
|
740
|
+
} catch (error) {
|
741
|
+
res.status(500).json({ error: 'Registration failed' });
|
742
|
+
}
|
743
|
+
}
|
744
|
+
```
|
745
|
+
|
746
|
+
```js
|
747
|
+
// middleware/requireAuth.js - Protected route middleware
|
748
|
+
export default function requireAuth(req, res, next) {
|
749
|
+
const token = req.headers.authorization?.split(' ')[1];
|
750
|
+
|
751
|
+
if (!token) {
|
752
|
+
return res.status(401).json({ error: 'Authentication required' });
|
753
|
+
}
|
754
|
+
|
755
|
+
try {
|
756
|
+
// Verify token
|
757
|
+
const decoded = req.auth.verifyToken(token);
|
758
|
+
req.user = decoded; // Attach user to request
|
759
|
+
next();
|
760
|
+
} catch (error) {
|
761
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
762
|
+
}
|
763
|
+
}
|
764
|
+
|
765
|
+
// api/profile.js - Protected route example
|
766
|
+
import requireAuth from '../middleware/requireAuth.js';
|
767
|
+
|
768
|
+
export const middleware = [requireAuth];
|
336
769
|
|
337
|
-
// Protected route
|
338
|
-
// api/profile.js
|
339
770
|
export async function get(req, res) {
|
340
|
-
// req.
|
341
|
-
|
342
|
-
|
771
|
+
// req.user is available from requireAuth middleware
|
772
|
+
const { id } = req.user;
|
773
|
+
|
774
|
+
const profile = await req.db.collection('users').findOne(
|
775
|
+
{ _id: new ObjectId(id) },
|
776
|
+
{ projection: { password: 0 } } // Exclude password
|
777
|
+
);
|
778
|
+
|
779
|
+
res.json(profile);
|
343
780
|
}
|
344
781
|
```
|
345
782
|
</details>
|
346
783
|
|
347
784
|
## 🔄 Server-Side Rendering
|
348
785
|
|
786
|
+
Frontend Hamroun provides built-in server-side rendering capabilities to improve performance and SEO:
|
787
|
+
|
349
788
|
<details>
|
350
|
-
<summary><b>
|
789
|
+
<summary><b>Server-Side Rendering Setup</b></summary>
|
351
790
|
|
352
791
|
```jsx
|
353
|
-
//
|
792
|
+
// server.js
|
793
|
+
import express from 'express';
|
354
794
|
import { renderToString } from 'frontend-hamroun/ssr';
|
355
795
|
import App from './App';
|
356
796
|
|
797
|
+
const app = express();
|
798
|
+
app.use(express.static('public'));
|
799
|
+
|
357
800
|
app.get('*', async (req, res) => {
|
801
|
+
// Render app to string
|
358
802
|
const html = await renderToString(<App url={req.url} />);
|
359
803
|
|
804
|
+
// Send complete HTML document
|
360
805
|
res.send(`
|
361
806
|
<!DOCTYPE html>
|
362
807
|
<html>
|
363
808
|
<head>
|
364
809
|
<title>My SSR App</title>
|
810
|
+
<meta charset="UTF-8">
|
811
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
365
812
|
<link rel="stylesheet" href="/styles.css">
|
366
813
|
</head>
|
367
814
|
<body>
|
368
815
|
<div id="root">${html}</div>
|
369
|
-
<script src="/
|
816
|
+
<script src="/bundle.js"></script>
|
370
817
|
</body>
|
371
818
|
</html>
|
372
819
|
`);
|
373
820
|
});
|
374
821
|
|
375
|
-
|
822
|
+
app.listen(3000, () => {
|
823
|
+
console.log('Server running at http://localhost:3000');
|
824
|
+
});
|
825
|
+
|
826
|
+
// client.js (for hydration)
|
376
827
|
import { hydrate } from 'frontend-hamroun';
|
377
828
|
import App from './App';
|
378
829
|
|
830
|
+
// Hydrate the app in the browser
|
379
831
|
hydrate(<App url={window.location.pathname} />, document.getElementById('root'));
|
380
832
|
```
|
381
833
|
</details>
|
382
834
|
|
383
|
-
##
|
835
|
+
## 🛠️ CLI Tools
|
836
|
+
|
837
|
+
Frontend Hamroun includes a powerful CLI for scaffolding projects, components, and API routes:
|
384
838
|
|
385
839
|
<details>
|
386
|
-
<summary><b>
|
840
|
+
<summary><b>Project Creation</b></summary>
|
387
841
|
|
388
|
-
|
842
|
+
```bash
|
843
|
+
# Create a new project with interactive prompts
|
844
|
+
npx frontend-hamroun create my-app
|
389
845
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
function handleSubmit() {
|
394
|
-
batchUpdates(() => {
|
395
|
-
setSubmitting(true);
|
396
|
-
setFormData({ name: '', email: '' });
|
397
|
-
setErrors({});
|
398
|
-
setSubmitCount(c => c + 1);
|
399
|
-
});
|
400
|
-
}
|
846
|
+
# Create with specific template
|
847
|
+
npx frontend-hamroun create my-app --template fullstack-app
|
401
848
|
```
|
402
|
-
</details>
|
403
849
|
|
404
|
-
|
850
|
+
Available templates:
|
851
|
+
- `basic-app`: Minimal client-side setup
|
852
|
+
- `ssr-template`: Server-side rendering with hydration
|
853
|
+
- `fullstack-app`: Complete solution with frontend, backend, auth, and DB
|
854
|
+
</details>
|
405
855
|
|
406
856
|
<details>
|
407
|
-
<summary><b>
|
857
|
+
<summary><b>Component Generation</b></summary>
|
408
858
|
|
409
859
|
```bash
|
410
|
-
# Create a new project
|
411
|
-
npx frontend-hamroun create my-app
|
412
|
-
|
413
860
|
# Generate a new component
|
414
861
|
npx frontend-hamroun add:component Button
|
415
862
|
|
416
|
-
#
|
417
|
-
npx frontend-hamroun add:
|
863
|
+
# Generate a TypeScript component
|
864
|
+
npx frontend-hamroun add:component UserProfile --typescript
|
418
865
|
|
419
|
-
#
|
420
|
-
npx frontend-hamroun add:
|
866
|
+
# Specify path and hooks
|
867
|
+
npx frontend-hamroun add:component Sidebar --path=src/layout --hooks=useState,useEffect
|
421
868
|
```
|
422
869
|
</details>
|
423
870
|
|
424
871
|
<details>
|
425
|
-
<summary><b>
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
872
|
+
<summary><b>API Route Generation</b></summary>
|
873
|
+
|
874
|
+
```bash
|
875
|
+
# Generate API route
|
876
|
+
npx frontend-hamroun add:api products
|
877
|
+
|
878
|
+
# Specify HTTP methods and auth requirement
|
879
|
+
npx frontend-hamroun add:api orders --methods=get,post --auth
|
880
|
+
```
|
881
|
+
</details>
|
882
|
+
|
883
|
+
## 🧩 Advanced Usage
|
884
|
+
|
885
|
+
<details>
|
886
|
+
<summary><b>Custom Hooks</b></summary>
|
887
|
+
|
888
|
+
```jsx
|
889
|
+
import { useState, useEffect } from 'frontend-hamroun';
|
890
|
+
|
891
|
+
// Custom hook for fetching data
|
892
|
+
function useFetch(url, options = {}) {
|
893
|
+
const [data, setData] = useState(null);
|
894
|
+
const [loading, setLoading] = useState(true);
|
895
|
+
const [error, setError] = useState(null);
|
896
|
+
|
897
|
+
useEffect(() => {
|
898
|
+
setLoading(true);
|
899
|
+
setError(null);
|
900
|
+
|
901
|
+
fetch(url, options)
|
902
|
+
.then(response => {
|
903
|
+
if (!response.ok) {
|
904
|
+
throw new Error(`HTTP error ${response.status}`);
|
905
|
+
}
|
906
|
+
return response.json();
|
907
|
+
})
|
908
|
+
.then(json => {
|
909
|
+
setData(json);
|
910
|
+
setLoading(false);
|
911
|
+
})
|
912
|
+
.catch(err => {
|
913
|
+
setError(err.message);
|
914
|
+
setLoading(false);
|
915
|
+
});
|
916
|
+
}, [url]);
|
917
|
+
|
918
|
+
return { data, loading, error };
|
919
|
+
}
|
920
|
+
|
921
|
+
// Usage
|
922
|
+
function UserList() {
|
923
|
+
const { data, loading, error } = useFetch('/api/users');
|
924
|
+
|
925
|
+
if (loading) return <div>Loading...</div>;
|
926
|
+
if (error) return <div>Error: {error}</div>;
|
927
|
+
|
928
|
+
return (
|
929
|
+
<ul>
|
930
|
+
{data.map(user => (
|
931
|
+
<li key={user.id}>{user.name}</li>
|
932
|
+
))}
|
933
|
+
</ul>
|
934
|
+
);
|
935
|
+
}
|
936
|
+
```
|
937
|
+
</details>
|
938
|
+
|
939
|
+
<details>
|
940
|
+
<summary><b>Performance Optimization Techniques</b></summary>
|
941
|
+
|
942
|
+
```jsx
|
943
|
+
import { useMemo, useState, useRef, batchUpdates } from 'frontend-hamroun';
|
944
|
+
|
945
|
+
function OptimizedList({ items }) {
|
946
|
+
const [filter, setFilter] = useState('');
|
947
|
+
const [sortOrder, setSortOrder] = useState('asc');
|
948
|
+
const prevItems = useRef(items);
|
949
|
+
|
950
|
+
// Memoize expensive calculations
|
951
|
+
const processedItems = useMemo(() => {
|
952
|
+
console.log('Processing items...');
|
953
|
+
let result = [...items];
|
954
|
+
|
955
|
+
// Apply filter
|
956
|
+
if (filter) {
|
957
|
+
result = result.filter(item =>
|
958
|
+
item.name.toLowerCase().includes(filter.toLowerCase())
|
959
|
+
);
|
960
|
+
}
|
961
|
+
|
962
|
+
// Apply sorting
|
963
|
+
result.sort((a, b) => {
|
964
|
+
const comparison = a.name.localeCompare(b.name);
|
965
|
+
return sortOrder === 'asc' ? comparison : -comparison;
|
966
|
+
});
|
967
|
+
|
968
|
+
return result;
|
969
|
+
}, [items, filter, sortOrder]);
|
970
|
+
|
971
|
+
// Check for changed items with more control than dependency arrays
|
972
|
+
if (items !== prevItems.current) {
|
973
|
+
console.log('Items array reference changed');
|
974
|
+
prevItems.current = items;
|
975
|
+
}
|
976
|
+
|
977
|
+
// Batch multiple state updates together
|
978
|
+
const toggleSortAndClear = () => {
|
979
|
+
batchUpdates(() => {
|
980
|
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
981
|
+
setFilter('');
|
982
|
+
});
|
983
|
+
};
|
984
|
+
|
985
|
+
return (
|
986
|
+
<div>
|
987
|
+
<div>
|
988
|
+
<input
|
989
|
+
type="text"
|
990
|
+
value={filter}
|
991
|
+
onChange={e => setFilter(e.target.value)}
|
992
|
+
placeholder="Filter items..."
|
993
|
+
/>
|
994
|
+
<button onClick={toggleSortAndClear}>
|
995
|
+
Toggle Sort ({sortOrder})
|
996
|
+
</button>
|
997
|
+
</div>
|
998
|
+
|
999
|
+
{/* Using key for efficient list rendering */}
|
1000
|
+
<ul>
|
1001
|
+
{processedItems.map(item => (
|
1002
|
+
<li key={item.id}>{item.name}</li>
|
1003
|
+
))}
|
1004
|
+
</ul>
|
1005
|
+
</div>
|
1006
|
+
);
|
1007
|
+
}
|
1008
|
+
```
|
441
1009
|
</details>
|
442
1010
|
|
443
1011
|
## 📝 TypeScript Support
|
444
1012
|
|
1013
|
+
Frontend Hamroun is built with TypeScript and comes with complete type definitions:
|
1014
|
+
|
445
1015
|
<details>
|
446
|
-
<summary><b>Type-Safe
|
1016
|
+
<summary><b>Type-Safe Component Example</b></summary>
|
447
1017
|
|
448
1018
|
```tsx
|
449
|
-
import { useState } from 'frontend-hamroun';
|
1019
|
+
import { useState, useEffect } from 'frontend-hamroun';
|
450
1020
|
|
451
|
-
|
1021
|
+
// Define props interface
|
1022
|
+
interface UserProfileProps {
|
452
1023
|
id: number;
|
453
1024
|
name: string;
|
454
1025
|
email: string;
|
1026
|
+
role: 'admin' | 'user' | 'guest';
|
1027
|
+
onStatusChange?: (id: number, active: boolean) => void;
|
455
1028
|
}
|
456
1029
|
|
457
|
-
|
458
|
-
|
1030
|
+
// Define state interface
|
1031
|
+
interface UserProfileState {
|
1032
|
+
isActive: boolean;
|
1033
|
+
isEditing: boolean;
|
1034
|
+
formData: {
|
1035
|
+
name: string;
|
1036
|
+
email: string;
|
1037
|
+
};
|
1038
|
+
}
|
1039
|
+
|
1040
|
+
function UserProfile({
|
1041
|
+
id,
|
1042
|
+
name,
|
1043
|
+
email,
|
1044
|
+
role,
|
1045
|
+
onStatusChange
|
1046
|
+
}: UserProfileProps) {
|
1047
|
+
// Type-safe state
|
1048
|
+
const [state, setState] = useState<UserProfileState>({
|
1049
|
+
isActive: true,
|
1050
|
+
isEditing: false,
|
1051
|
+
formData: {
|
1052
|
+
name,
|
1053
|
+
email
|
1054
|
+
}
|
1055
|
+
});
|
1056
|
+
|
1057
|
+
// Type-safe event handlers
|
1058
|
+
const toggleStatus = (): void => {
|
1059
|
+
const newStatus = !state.isActive;
|
1060
|
+
setState(prev => ({
|
1061
|
+
...prev,
|
1062
|
+
isActive: newStatus
|
1063
|
+
}));
|
1064
|
+
|
1065
|
+
if (onStatusChange) {
|
1066
|
+
onStatusChange(id, newStatus);
|
1067
|
+
}
|
1068
|
+
};
|
1069
|
+
|
1070
|
+
// Type-safe refs
|
1071
|
+
const formRef = useRef<HTMLFormElement>(null);
|
459
1072
|
|
460
1073
|
return (
|
461
1074
|
<div className="user-profile">
|
462
1075
|
<h2>{name}</h2>
|
463
|
-
<p>{email}</p>
|
464
|
-
<
|
465
|
-
|
1076
|
+
<p>Email: {email}</p>
|
1077
|
+
<p>Role: {role}</p>
|
1078
|
+
<p>Status: {state.isActive ? 'Active' : 'Inactive'}</p>
|
1079
|
+
|
1080
|
+
<button onClick={toggleStatus}>
|
1081
|
+
{state.isActive ? 'Deactivate' : 'Activate'}
|
466
1082
|
</button>
|
1083
|
+
|
1084
|
+
{state.isEditing ? (
|
1085
|
+
<form ref={formRef}>
|
1086
|
+
{/* Form fields */}
|
1087
|
+
</form>
|
1088
|
+
) : (
|
1089
|
+
<button onClick={() => setState(prev => ({
|
1090
|
+
...prev,
|
1091
|
+
isEditing: true
|
1092
|
+
}))}>
|
1093
|
+
Edit
|
1094
|
+
</button>
|
1095
|
+
)}
|
467
1096
|
</div>
|
468
1097
|
);
|
469
1098
|
}
|
1099
|
+
|
1100
|
+
// Usage with type checking
|
1101
|
+
const App = () => (
|
1102
|
+
<div>
|
1103
|
+
<UserProfile
|
1104
|
+
id={1}
|
1105
|
+
name="John Doe"
|
1106
|
+
email="john@example.com"
|
1107
|
+
role="admin"
|
1108
|
+
onStatusChange={(id, active) => {
|
1109
|
+
console.log(`User ${id} status changed to ${active}`);
|
1110
|
+
}}
|
1111
|
+
/>
|
1112
|
+
|
1113
|
+
{/* This would cause TypeScript errors */}
|
1114
|
+
{/*
|
1115
|
+
<UserProfile
|
1116
|
+
id="1" // Error: string is not assignable to number
|
1117
|
+
name="Jane Doe"
|
1118
|
+
role="manager" // Error: 'manager' is not assignable
|
1119
|
+
/>
|
1120
|
+
*/}
|
1121
|
+
</div>
|
1122
|
+
);
|
1123
|
+
```
|
1124
|
+
</details>
|
1125
|
+
|
1126
|
+
<details>
|
1127
|
+
<summary><b>Type-Safe API Routes</b></summary>
|
1128
|
+
|
1129
|
+
```ts
|
1130
|
+
// types.ts
|
1131
|
+
export interface User {
|
1132
|
+
id?: string;
|
1133
|
+
name: string;
|
1134
|
+
email: string;
|
1135
|
+
password?: string;
|
1136
|
+
role: 'admin' | 'user';
|
1137
|
+
createdAt?: Date;
|
1138
|
+
updatedAt?: Date;
|
1139
|
+
}
|
1140
|
+
|
1141
|
+
// api/users/[id].ts
|
1142
|
+
import type { User } from '../../types';
|
1143
|
+
|
1144
|
+
export async function get(req, res) {
|
1145
|
+
const { id } = req.params;
|
1146
|
+
|
1147
|
+
try {
|
1148
|
+
const user = await req.db.collection('users').findOne({
|
1149
|
+
_id: new ObjectId(id)
|
1150
|
+
}) as User;
|
1151
|
+
|
1152
|
+
if (!user) {
|
1153
|
+
return res.status(404).json({ error: 'User not found' });
|
1154
|
+
}
|
1155
|
+
|
1156
|
+
// Omit sensitive data
|
1157
|
+
const { password, ...safeUser } = user;
|
1158
|
+
|
1159
|
+
res.json(safeUser);
|
1160
|
+
} catch (error) {
|
1161
|
+
res.status(500).json({ error: 'Server error' });
|
1162
|
+
}
|
1163
|
+
}
|
1164
|
+
|
1165
|
+
export async function put(req, res) {
|
1166
|
+
const { id } = req.params;
|
1167
|
+
const { name, email, role } = req.body as Partial<User>;
|
1168
|
+
|
1169
|
+
// Validate that role is a valid enum value
|
1170
|
+
if (role && !['admin', 'user'].includes(role)) {
|
1171
|
+
return res.status(400).json({ error: 'Invalid role' });
|
1172
|
+
}
|
1173
|
+
|
1174
|
+
try {
|
1175
|
+
await req.db.collection('users').updateOne(
|
1176
|
+
{ _id: new ObjectId(id) },
|
1177
|
+
{
|
1178
|
+
$set: {
|
1179
|
+
...(name && { name }),
|
1180
|
+
...(email && { email }),
|
1181
|
+
...(role && { role }),
|
1182
|
+
updatedAt: new Date()
|
1183
|
+
}
|
1184
|
+
}
|
1185
|
+
);
|
1186
|
+
|
1187
|
+
res.json({ success: true });
|
1188
|
+
} catch (error) {
|
1189
|
+
res.status(500).json({ error: 'Server error' });
|
1190
|
+
}
|
1191
|
+
}
|
470
1192
|
```
|
471
1193
|
</details>
|
472
1194
|
|
473
1195
|
## 🌐 Browser Compatibility
|
474
1196
|
|
475
|
-
Frontend Hamroun
|
476
|
-
|
477
|
-
-
|
1197
|
+
Frontend Hamroun supports all modern browsers out of the box:
|
1198
|
+
|
1199
|
+
- Chrome (latest 2 versions)
|
1200
|
+
- Firefox (latest 2 versions)
|
1201
|
+
- Safari (latest 2 versions)
|
1202
|
+
- Edge (latest 2 versions)
|
1203
|
+
|
1204
|
+
For older browsers like IE11, you'll need:
|
1205
|
+
- Appropriate polyfills
|
1206
|
+
- Transpilation to ES5
|
1207
|
+
- CSS compatibility work
|
1208
|
+
|
1209
|
+
## ❓ Frequently Asked Questions
|
1210
|
+
|
1211
|
+
<details>
|
1212
|
+
<summary><b>How does Frontend Hamroun compare to React?</b></summary>
|
1213
|
+
|
1214
|
+
Frontend Hamroun offers a React-like API but with additional built-in features:
|
1215
|
+
- Smaller bundle size (~5KB vs. 42KB+)
|
1216
|
+
- Integrated backend capabilities
|
1217
|
+
- Built-in server-side rendering
|
1218
|
+
- Database integrations
|
1219
|
+
- Authentication system
|
478
1220
|
|
479
|
-
|
1221
|
+
For React developers, the learning curve is minimal.
|
1222
|
+
</details>
|
480
1223
|
|
481
|
-
|
1224
|
+
<details>
|
1225
|
+
<summary><b>Can I use it with existing React components?</b></summary>
|
1226
|
+
|
1227
|
+
While Frontend Hamroun's API is similar to React's, they have different internal implementations. You can:
|
1228
|
+
- Port React components to Frontend Hamroun with minimal changes
|
1229
|
+
- Use React components in dedicated sections with a compatibility layer
|
1230
|
+
- Share non-UI code and logic between both frameworks
|
1231
|
+
</details>
|
1232
|
+
|
1233
|
+
<details>
|
1234
|
+
<summary><b>Does it support static site generation (SSG)?</b></summary>
|
1235
|
+
|
1236
|
+
Yes, Frontend Hamroun provides tools for static site generation. Use the `renderToString` API to pre-render pages at build time.
|
1237
|
+
</details>
|
482
1238
|
|
483
1239
|
## 📄 License
|
484
1240
|
|
@@ -487,5 +1243,5 @@ MIT © Hamroun
|
|
487
1243
|
---
|
488
1244
|
|
489
1245
|
<p align="center">
|
490
|
-
<a href="#-
|
1246
|
+
<a href="#-table-of-contents">⬆️ Back to top</a>
|
491
1247
|
</p>
|