create-aziz-app 3.1.0 → 3.1.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/bin/create-aziz-app.js +9 -58
- package/package.json +1 -1
- package/template/README.md +402 -667
- package/template/app/layout.js +18 -20
- package/template/app/page.js +33 -56
- package/template/scripts/lib/js-transform.js +57 -3
- package/template/api/contact.js +0 -39
- package/template/api/createUser.js +0 -69
- package/template/api/hello.js +0 -17
- package/template/api/loginUser.js +0 -61
- package/template/api/users.js +0 -59
- package/template/app/about/page.js +0 -41
- package/template/app/blog/[slug]/page.js +0 -31
- package/template/app/contact/page.js +0 -68
- package/template/app/not-found.js +0 -15
- package/template/app/test/page.js +0 -52
- package/template/components/Footer.js +0 -7
- package/template/components/Navbar.js +0 -28
- package/template/lib/db.js +0 -67
- package/template/models/User.js +0 -102
package/template/app/layout.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
description: 'Built with Aziz.js'
|
|
20
|
-
};
|
|
1
|
+
// Root layout — wraps every page (like Next.js app/layout.js)
|
|
2
|
+
export default function Layout({ children }) {
|
|
3
|
+
return (
|
|
4
|
+
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
|
5
|
+
<main style={{ flex: 1 }}>
|
|
6
|
+
{children}
|
|
7
|
+
</main>
|
|
8
|
+
<footer>
|
|
9
|
+
Built with Aziz.js
|
|
10
|
+
</footer>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const metadata = {
|
|
16
|
+
title: 'My App',
|
|
17
|
+
description: 'Built with Aziz.js'
|
|
18
|
+
};
|
package/template/app/page.js
CHANGED
|
@@ -1,56 +1,33 @@
|
|
|
1
|
-
// Home page — route: /
|
|
2
|
-
export default function Page() {
|
|
3
|
-
const [count, setCount] = useState(0);
|
|
4
|
-
|
|
5
|
-
return (
|
|
6
|
-
<div style={{ padding: '
|
|
7
|
-
<h1>Welcome
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Contact →
|
|
35
|
-
</button>
|
|
36
|
-
<button class="btn btn-primary" onClick={() => navigate('/test')}>
|
|
37
|
-
Go to Test →
|
|
38
|
-
</button>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div class="card">
|
|
43
|
-
<h2>Import Anything</h2>
|
|
44
|
-
<p>
|
|
45
|
-
Use <code>import</code> statements just like Next.js.
|
|
46
|
-
The dev server resolves bare imports from <code>node_modules</code> automatically.
|
|
47
|
-
</p>
|
|
48
|
-
<pre style={{ background: '#282c34', color: '#61dafb', padding: '16px', borderRadius: '8px', overflow: 'auto', fontSize: '13px' }}>
|
|
49
|
-
{`import mongoose from 'mongoose';
|
|
50
|
-
import connectDB from '@/lib/db';
|
|
51
|
-
import User from '@/models/User';`}
|
|
52
|
-
</pre>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
1
|
+
// Home page — route: /
|
|
2
|
+
export default function Page() {
|
|
3
|
+
const [count, setCount] = useState(0);
|
|
4
|
+
|
|
5
|
+
return (
|
|
6
|
+
<div style={{ padding: '48px 32px', maxWidth: '720px', margin: '0 auto' }}>
|
|
7
|
+
<h1>Welcome to Aziz.js</h1>
|
|
8
|
+
<p>
|
|
9
|
+
Your app is running. Edit <code>app/page.js</code> and save —
|
|
10
|
+
the browser refreshes automatically.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<div class="card">
|
|
14
|
+
<h2>Interactive state</h2>
|
|
15
|
+
<p>A tiny slice of state, exactly like React's <code>useState</code>.</p>
|
|
16
|
+
<div class="count-display">{count}</div>
|
|
17
|
+
<div class="counter-actions">
|
|
18
|
+
<button class="btn btn-danger" onClick={() => setCount(c => c - 1)}>−</button>
|
|
19
|
+
<button class="btn btn-primary" onClick={() => setCount(c => c + 1)}>+</button>
|
|
20
|
+
<button class="btn btn-grey" onClick={() => setCount(0)}>Reset</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="card">
|
|
25
|
+
<h2>Add a page</h2>
|
|
26
|
+
<p>
|
|
27
|
+
Each <code>page.js</code> inside <code>app/</code> is a route.
|
|
28
|
+
Create <code>app/about/page.js</code> and it becomes <code>/about</code>.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -53,6 +53,55 @@ function transformJSX(code, filename) {
|
|
|
53
53
|
//
|
|
54
54
|
// Runs on the whole module (not line-by-line) so multi-line
|
|
55
55
|
// import statements are handled correctly.
|
|
56
|
+
//
|
|
57
|
+
// Only keywords sitting in CODE position are rewritten. A `from`
|
|
58
|
+
// or `import` inside a string literal / template literal / comment
|
|
59
|
+
// is prose (e.g. documentation text in JSX) and must be left alone
|
|
60
|
+
// — rewriting it corrupts the module and the page fails to load.
|
|
61
|
+
|
|
62
|
+
// Uint8Array marking every character that is in code position
|
|
63
|
+
// (outside string literals, template literals and comments).
|
|
64
|
+
function codePositions(code) {
|
|
65
|
+
const n = code.length;
|
|
66
|
+
const isCode = new Uint8Array(n);
|
|
67
|
+
const stack = []; // '{' for blocks, 'tpl' for ${ … } in templates
|
|
68
|
+
let mode = 'code'; // code | ' | " | ` | // | /*
|
|
69
|
+
let i = 0;
|
|
70
|
+
|
|
71
|
+
while (i < n) {
|
|
72
|
+
const c = code[i];
|
|
73
|
+
|
|
74
|
+
if (mode === 'code') {
|
|
75
|
+
isCode[i] = 1;
|
|
76
|
+
if (c === '\\') { i += 2; continue; }
|
|
77
|
+
if (c === "'" || c === '"' || c === '`') mode = c;
|
|
78
|
+
else if (c === '/' && code[i + 1] === '/') mode = '//';
|
|
79
|
+
else if (c === '/' && code[i + 1] === '*') mode = '/*';
|
|
80
|
+
else if (c === '{') stack.push('{');
|
|
81
|
+
else if (c === '}') { if (stack.pop() === 'tpl') mode = '`'; }
|
|
82
|
+
i++;
|
|
83
|
+
} else if (mode === "'" || mode === '"' || mode === '`') {
|
|
84
|
+
if (c === '\\') { i += 2; continue; }
|
|
85
|
+
if (c === mode) mode = 'code';
|
|
86
|
+
else if (mode === '`' && c === '$' && code[i + 1] === '{') {
|
|
87
|
+
stack.push('tpl');
|
|
88
|
+
mode = 'code';
|
|
89
|
+
isCode[i] = 1;
|
|
90
|
+
isCode[i + 1] = 1;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
i++;
|
|
95
|
+
} else if (mode === '//') {
|
|
96
|
+
if (c === '\n') mode = 'code';
|
|
97
|
+
i++;
|
|
98
|
+
} else { // block comment
|
|
99
|
+
if (c === '*' && code[i + 1] === '/') { mode = 'code'; i += 2; continue; }
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return isCode;
|
|
104
|
+
}
|
|
56
105
|
|
|
57
106
|
function isRelativeOrAbsolute(spec) {
|
|
58
107
|
return spec.startsWith('/') ||
|
|
@@ -72,21 +121,26 @@ function mapSpecifier(spec) {
|
|
|
72
121
|
}
|
|
73
122
|
|
|
74
123
|
function rewriteImports(code) {
|
|
124
|
+
const isCode = codePositions(code);
|
|
125
|
+
|
|
75
126
|
// from 'specifier' (covers `import ... from`, `export ... from`)
|
|
76
|
-
code = code.replace(/(\bfrom\s*)(['"])([^'"\n]+)\2/g, (m, from, q, spec) => {
|
|
127
|
+
code = code.replace(/(\bfrom\s*)(['"])([^'"\n]+)\2/g, (m, from, q, spec, offset) => {
|
|
128
|
+
if (!isCode[offset]) return m;
|
|
77
129
|
const mapped = mapSpecifier(spec);
|
|
78
130
|
return mapped === spec ? m : `${from}${q}${mapped}${q}`;
|
|
79
131
|
});
|
|
80
132
|
|
|
81
133
|
// side-effect imports: import 'specifier';
|
|
82
134
|
// (requires the quote right after `import`, so `import(` is not matched)
|
|
83
|
-
code = code.replace(/(\bimport\s+)(['"])([^'"\n]+)\2/g, (m, kw, q, spec) => {
|
|
135
|
+
code = code.replace(/(\bimport\s+)(['"])([^'"\n]+)\2/g, (m, kw, q, spec, offset) => {
|
|
136
|
+
if (!isCode[offset]) return m;
|
|
84
137
|
const mapped = mapSpecifier(spec);
|
|
85
138
|
return mapped === spec ? m : `${kw}${q}${mapped}${q}`;
|
|
86
139
|
});
|
|
87
140
|
|
|
88
141
|
// dynamic imports: import('specifier')
|
|
89
|
-
code = code.replace(/(\bimport\s*\(\s*)(['"])([^'"\n]+)\2(\s*\))/g, (m, pre, q, spec, post) => {
|
|
142
|
+
code = code.replace(/(\bimport\s*\(\s*)(['"])([^'"\n]+)\2(\s*\))/g, (m, pre, q, spec, post, offset) => {
|
|
143
|
+
if (!isCode[offset]) return m;
|
|
90
144
|
const mapped = mapSpecifier(spec);
|
|
91
145
|
return mapped === spec ? m : `${pre}${q}${mapped}${q}${post}`;
|
|
92
146
|
});
|
package/template/api/contact.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// API Route: POST /api/contact (classic style — api/contact.js)
|
|
2
|
-
//
|
|
3
|
-
// Demonstrates validation, cookies, and NextResponse helpers.
|
|
4
|
-
|
|
5
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
6
|
-
|
|
7
|
-
// In-memory store (replace with a real database in production)
|
|
8
|
-
const messages = [];
|
|
9
|
-
|
|
10
|
-
async function GET(request) {
|
|
11
|
-
return NextResponse.json({ count: messages.length, messages });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function POST(request) {
|
|
15
|
-
let body;
|
|
16
|
-
try {
|
|
17
|
-
body = await request.json();
|
|
18
|
-
} catch (err) {
|
|
19
|
-
return NextResponse.json({ error: 'Invalid JSON body' }, { status: err.status || 400 });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const { name, email, message } = body;
|
|
23
|
-
|
|
24
|
-
if (!name || !email || !message) {
|
|
25
|
-
return NextResponse.json(
|
|
26
|
-
{ error: 'name, email and message are required' },
|
|
27
|
-
{ status: 400 }
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const entry = { name, email, message, at: new Date().toISOString() };
|
|
32
|
-
messages.push(entry);
|
|
33
|
-
|
|
34
|
-
const res = NextResponse.json({ message: `Thanks, ${name}! We received your message.`, entry }, { status: 201 });
|
|
35
|
-
res.cookies.set('last_contact', name, { path: '/', maxAge: 60 * 60 });
|
|
36
|
-
return res;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
module.exports = { GET, POST };
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// API Route: /api/createUser
|
|
2
|
-
// Next.js App Router style
|
|
3
|
-
|
|
4
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
5
|
-
|
|
6
|
-
// Mock database
|
|
7
|
-
let users = [
|
|
8
|
-
{ _id: '1', name: 'Aziz Ahmed', email: 'aziz@example.com', role: 'admin', phone: '123-456-7890' }
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
// POST handler
|
|
12
|
-
async function POST(request) {
|
|
13
|
-
try {
|
|
14
|
-
// Parse request body
|
|
15
|
-
const body = await request.json();
|
|
16
|
-
const { name, email, password, role, phone } = body;
|
|
17
|
-
|
|
18
|
-
// Validation
|
|
19
|
-
if (!name || !email || !password) {
|
|
20
|
-
return NextResponse.json(
|
|
21
|
-
{
|
|
22
|
-
error: 'Validation failed',
|
|
23
|
-
message: 'Name, email, and password are required'
|
|
24
|
-
},
|
|
25
|
-
{ status: 400 }
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Check if email already exists
|
|
30
|
-
if (users.find(u => u.email === email)) {
|
|
31
|
-
return NextResponse.json(
|
|
32
|
-
{ error: 'User with this email already exists' },
|
|
33
|
-
{ status: 409 }
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Create new user
|
|
38
|
-
const newUser = {
|
|
39
|
-
_id: String(users.length + 1),
|
|
40
|
-
name,
|
|
41
|
-
email,
|
|
42
|
-
password, // In production, hash this with bcrypt!
|
|
43
|
-
role: role || 'user',
|
|
44
|
-
phone: phone || '',
|
|
45
|
-
createdAt: new Date().toISOString()
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
users.push(newUser);
|
|
49
|
-
|
|
50
|
-
// Return user data (without password)
|
|
51
|
-
const { password: _, ...userData } = newUser;
|
|
52
|
-
|
|
53
|
-
return NextResponse.json(
|
|
54
|
-
{
|
|
55
|
-
message: 'User created successfully',
|
|
56
|
-
user: userData
|
|
57
|
-
},
|
|
58
|
-
{ status: 201 }
|
|
59
|
-
);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error('Create user error:', error);
|
|
62
|
-
return NextResponse.json(
|
|
63
|
-
{ error: 'Failed to create user' },
|
|
64
|
-
{ status: 500 }
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = { POST };
|
package/template/api/hello.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// API Route: /api/hello
|
|
2
|
-
// Next.js App Router style
|
|
3
|
-
|
|
4
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
5
|
-
|
|
6
|
-
async function GET(request) {
|
|
7
|
-
const { searchParams } = new URL(request.url);
|
|
8
|
-
const name = searchParams.get('name') || 'World';
|
|
9
|
-
|
|
10
|
-
return NextResponse.json({
|
|
11
|
-
message: `Hello, ${name}!`,
|
|
12
|
-
timestamp: new Date().toISOString(),
|
|
13
|
-
method: request.method
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
module.exports = { GET };
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// API Route: /api/loginUser
|
|
2
|
-
// Next.js App Router style
|
|
3
|
-
|
|
4
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
5
|
-
|
|
6
|
-
// Mock database
|
|
7
|
-
const users = [
|
|
8
|
-
{ _id: '1', name: 'Aziz Ahmed', email: 'aziz@example.com', password: 'admin123', role: 'admin' },
|
|
9
|
-
{ _id: '2', name: 'John Doe', email: 'john@example.com', password: 'user123', role: 'user' }
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
// POST handler
|
|
13
|
-
async function POST(request) {
|
|
14
|
-
try {
|
|
15
|
-
// Parse request body
|
|
16
|
-
const body = await request.json();
|
|
17
|
-
const { email, password } = body;
|
|
18
|
-
|
|
19
|
-
// Validation
|
|
20
|
-
if (!email || !password) {
|
|
21
|
-
return NextResponse.json(
|
|
22
|
-
{ error: 'Email and password are required' },
|
|
23
|
-
{ status: 400 }
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Find user
|
|
28
|
-
const user = users.find(u => u.email === email);
|
|
29
|
-
|
|
30
|
-
if (!user) {
|
|
31
|
-
return NextResponse.json(
|
|
32
|
-
{ error: 'Invalid credentials' },
|
|
33
|
-
{ status: 401 }
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check password (in production, use bcrypt!)
|
|
38
|
-
if (user.password !== password) {
|
|
39
|
-
return NextResponse.json(
|
|
40
|
-
{ error: 'Invalid credentials' },
|
|
41
|
-
{ status: 401 }
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Return user data (without password)
|
|
46
|
-
const { password: _, ...userData } = user;
|
|
47
|
-
|
|
48
|
-
return NextResponse.json({
|
|
49
|
-
...userData,
|
|
50
|
-
message: 'Login successful'
|
|
51
|
-
});
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error('Login error:', error);
|
|
54
|
-
return NextResponse.json(
|
|
55
|
-
{ error: 'Failed to login' },
|
|
56
|
-
{ status: 500 }
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
module.exports = { POST };
|
package/template/api/users.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// API Route: /api/users
|
|
2
|
-
// Next.js App Router style
|
|
3
|
-
|
|
4
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
5
|
-
|
|
6
|
-
let users = [
|
|
7
|
-
{ id: 1, name: 'Aziz Ahmed', email: 'aziz@example.com', createdAt: new Date().toISOString() },
|
|
8
|
-
{ id: 2, name: 'John Doe', email: 'john@example.com', createdAt: new Date().toISOString() },
|
|
9
|
-
{ id: 3, name: 'Jane Smith', email: 'jane@example.com', createdAt: new Date().toISOString() }
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
async function GET(request) {
|
|
13
|
-
const { searchParams } = new URL(request.url);
|
|
14
|
-
const id = searchParams.get('id');
|
|
15
|
-
|
|
16
|
-
if (id) {
|
|
17
|
-
const user = users.find(u => u.id === parseInt(id));
|
|
18
|
-
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
19
|
-
return NextResponse.json(user);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return NextResponse.json({ users, total: users.length });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function POST(request) {
|
|
26
|
-
const { name, email } = await request.json();
|
|
27
|
-
|
|
28
|
-
if (!name || !email) {
|
|
29
|
-
return NextResponse.json({ error: 'Name and email are required' }, { status: 400 });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (users.find(u => u.email === email)) {
|
|
33
|
-
return NextResponse.json({ error: 'Email already exists' }, { status: 409 });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const newUser = {
|
|
37
|
-
id: Math.max(...users.map(u => u.id)) + 1,
|
|
38
|
-
name, email,
|
|
39
|
-
createdAt: new Date().toISOString()
|
|
40
|
-
};
|
|
41
|
-
users.push(newUser);
|
|
42
|
-
|
|
43
|
-
return NextResponse.json({ message: 'User created', user: newUser }, { status: 201 });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function DELETE(request) {
|
|
47
|
-
const { searchParams } = new URL(request.url);
|
|
48
|
-
const id = searchParams.get('id');
|
|
49
|
-
|
|
50
|
-
if (!id) return NextResponse.json({ error: 'ID required' }, { status: 400 });
|
|
51
|
-
|
|
52
|
-
const idx = users.findIndex(u => u.id === parseInt(id));
|
|
53
|
-
if (idx === -1) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
54
|
-
|
|
55
|
-
const deleted = users.splice(idx, 1)[0];
|
|
56
|
-
return NextResponse.json({ message: 'User deleted', user: deleted });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
module.exports = { GET, POST, DELETE };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// About page — route: /about
|
|
2
|
-
export default function Page() {
|
|
3
|
-
return (
|
|
4
|
-
<div style={{ padding: '40px 32px', maxWidth: '900px', margin: '0 auto' }}>
|
|
5
|
-
<h1>About 📖</h1>
|
|
6
|
-
|
|
7
|
-
<div class="card">
|
|
8
|
-
<h2>What is Aziz.js?</h2>
|
|
9
|
-
<p>
|
|
10
|
-
A lightweight Next.js clone — file-based routing, React hooks,
|
|
11
|
-
server-side API routes, and ES module imports. Zero dependencies.
|
|
12
|
-
</p>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<div class="card">
|
|
16
|
-
<h2>File Structure</h2>
|
|
17
|
-
<pre style={{ background: '#282c34', color: '#61dafb', padding: '16px', borderRadius: '8px', fontSize: '13px' }}>
|
|
18
|
-
{`app/
|
|
19
|
-
layout.js ← root layout
|
|
20
|
-
page.js ← /
|
|
21
|
-
about/
|
|
22
|
-
page.js ← /about
|
|
23
|
-
blog/
|
|
24
|
-
[slug]/
|
|
25
|
-
page.js ← /blog/:slug
|
|
26
|
-
|
|
27
|
-
api/
|
|
28
|
-
users.js ← /api/users
|
|
29
|
-
|
|
30
|
-
components/
|
|
31
|
-
Navbar.js
|
|
32
|
-
Footer.js`}
|
|
33
|
-
</pre>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<button class="btn btn-primary" onClick={() => navigate('/')}>
|
|
37
|
-
← Back Home
|
|
38
|
-
</button>
|
|
39
|
-
</div>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Blog post page — route: /blog/:slug (dynamic segment)
|
|
2
|
-
export default function BlogPost() {
|
|
3
|
-
const { slug } = useParams();
|
|
4
|
-
const search = useSearchParams();
|
|
5
|
-
const from = search.get('from');
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<div style={{ padding: '40px 32px', maxWidth: '900px', margin: '0 auto' }}>
|
|
9
|
-
<p style={{ color: '#888', marginBottom: '8px' }}>
|
|
10
|
-
<a href="/blog/hello-world" onClick={(e) => { e.preventDefault(); navigate('/blog/hello-world'); }}>hello-world</a>
|
|
11
|
-
{' · '}
|
|
12
|
-
<a href="/blog/aziz-js" onClick={(e) => { e.preventDefault(); navigate('/blog/aziz-js'); }}>aziz-js</a>
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
<h1>Blog: {slug}</h1>
|
|
16
|
-
|
|
17
|
-
<div class="card">
|
|
18
|
-
<h2>Dynamic routing works!</h2>
|
|
19
|
-
<p>
|
|
20
|
-
This page is <code>app/blog/[slug]/page.js</code> and it matches any
|
|
21
|
-
<code>/blog/<anything></code> URL. The matched value is available
|
|
22
|
-
through the <code>useParams()</code> hook.
|
|
23
|
-
</p>
|
|
24
|
-
<p><strong>useParams().slug</strong> = <code>{slug}</code></p>
|
|
25
|
-
{from && <p><strong>?from=</strong> = <code>{from}</code></p>}
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<button class="btn btn-primary" onClick={() => navigate('/')}>← Back Home</button>
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// Contact page — route: /contact
|
|
2
|
-
// Demonstrates the global `api` client calling a server route handler.
|
|
3
|
-
export default function ContactPage() {
|
|
4
|
-
const [name, setName] = useState('');
|
|
5
|
-
const [email, setEmail] = useState('');
|
|
6
|
-
const [message, setMessage] = useState('');
|
|
7
|
-
const [status, setStatus] = useState(null); // { type, text }
|
|
8
|
-
const [sending, setSending] = useState(false);
|
|
9
|
-
|
|
10
|
-
const handleSubmit = async (e) => {
|
|
11
|
-
e.preventDefault();
|
|
12
|
-
setSending(true);
|
|
13
|
-
setStatus(null);
|
|
14
|
-
try {
|
|
15
|
-
const res = await api.post('/api/contact', { name, email, message });
|
|
16
|
-
setStatus({ type: 'ok', text: res.message || 'Message sent!' });
|
|
17
|
-
setName(''); setEmail(''); setMessage('');
|
|
18
|
-
} catch (err) {
|
|
19
|
-
setStatus({ type: 'error', text: err.message || 'Something went wrong' });
|
|
20
|
-
} finally {
|
|
21
|
-
setSending(false);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div style={{ padding: '40px 32px', maxWidth: '640px', margin: '0 auto' }}>
|
|
27
|
-
<h1>Contact 📬</h1>
|
|
28
|
-
|
|
29
|
-
<div class="card">
|
|
30
|
-
<h2>Send us a message</h2>
|
|
31
|
-
<p>This form posts to <code>/api/contact</code> using the global <code>api</code> client.</p>
|
|
32
|
-
|
|
33
|
-
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '16px' }}>
|
|
34
|
-
<input
|
|
35
|
-
type="text"
|
|
36
|
-
placeholder="Your name"
|
|
37
|
-
value={name}
|
|
38
|
-
onInput={(e) => setName(e.target.value)}
|
|
39
|
-
required
|
|
40
|
-
/>
|
|
41
|
-
<input
|
|
42
|
-
type="email"
|
|
43
|
-
placeholder="Your email"
|
|
44
|
-
value={email}
|
|
45
|
-
onInput={(e) => setEmail(e.target.value)}
|
|
46
|
-
required
|
|
47
|
-
/>
|
|
48
|
-
<textarea
|
|
49
|
-
placeholder="Your message"
|
|
50
|
-
value={message}
|
|
51
|
-
onInput={(e) => setMessage(e.target.value)}
|
|
52
|
-
rows={4}
|
|
53
|
-
required
|
|
54
|
-
/>
|
|
55
|
-
<button type="submit" class="btn btn-primary" disabled={sending}>
|
|
56
|
-
{sending ? 'Sending…' : 'Send message'}
|
|
57
|
-
</button>
|
|
58
|
-
</form>
|
|
59
|
-
|
|
60
|
-
{status && (
|
|
61
|
-
<p style={{ marginTop: '12px', color: status.type === 'ok' ? '#1a7f37' : '#c0392b' }}>
|
|
62
|
-
{status.text}
|
|
63
|
-
</p>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Custom 404 page — rendered when no route matches (like Next.js app/not-found.js)
|
|
2
|
-
export default function NotFound() {
|
|
3
|
-
return (
|
|
4
|
-
<div style={{ padding: '80px 24px', textAlign: 'center' }}>
|
|
5
|
-
<h1 style={{ fontSize: '96px', margin: '0 0 8px', color: '#61dafb' }}>404</h1>
|
|
6
|
-
<h2 style={{ margin: '0 0 16px' }}>Page not found</h2>
|
|
7
|
-
<p style={{ color: '#888', marginBottom: '32px' }}>
|
|
8
|
-
We couldn't find the page you were looking for.
|
|
9
|
-
</p>
|
|
10
|
-
<button class="btn btn-primary" onClick={() => navigate('/')}>
|
|
11
|
-
← Back to Home
|
|
12
|
-
</button>
|
|
13
|
-
</div>
|
|
14
|
-
);
|
|
15
|
-
}
|