asjs-express 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asjs-express",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Lightweight Express view engine with EJS-like templates, layouts, async page rendering, form enhancement, and a built-in client router.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -5,6 +5,7 @@ const app = express();
5
5
  const port = process.env.PORT || __PORT__;
6
6
 
7
7
  app.use(express.urlencoded({ extended: false }));
8
+ app.use(express.json());
8
9
 
9
10
  const asjs = setupAsjs(app, {
10
11
  rootDir: __dirname,
@@ -12,7 +13,7 @@ const asjs = setupAsjs(app, {
12
13
  navItems: [
13
14
  { href: '/', label: 'Home', activeMode: 'exact' },
14
15
  { href: '/about', label: 'About', activeMode: 'exact' },
15
- { href: '/contact', label: 'Contact', activeMode: 'exact' }
16
+ { href: '/form', label: 'Contact', activeMode: 'exact' }
16
17
  ],
17
18
  brand: {
18
19
  href: '/',
@@ -25,32 +26,30 @@ const asjs = setupAsjs(app, {
25
26
  loadingBar: true
26
27
  });
27
28
 
28
- // Home
29
29
  app.get('/', asjs.page('home', {
30
30
  title: '__APP_TITLE__',
31
31
  headline: 'Welcome to __APP_TITLE__',
32
- description: 'A three-page starter built with ASJS and Express. SPA navigation, smooth transitions, and server-rendered pages are already set up.',
32
+ description: 'A starter built with ASJS and Express. SPA navigation, smooth transitions, and server-rendered pages are ready out of the box.',
33
33
  features: [
34
34
  {
35
35
  label: 'SPA Navigation',
36
- text: 'Pages swap without a full reload. The header and layout stay in place while only the content area changes.'
36
+ text: 'Pages swap without a full reload. The header and layout stay in place while only the content changes.'
37
37
  },
38
38
  {
39
39
  label: 'Server Rendering',
40
- text: 'Each page is fully rendered on the server. Route data is prepared in the callback before the HTML response is sent.'
40
+ text: 'Each page is fully rendered on the server before it reaches the browser. Route data is prepared in the callback.'
41
41
  },
42
42
  {
43
- label: 'Form Handling',
44
- text: 'Forms submit to the server and return a new rendered state. Validation errors are shown without losing the page shell.'
43
+ label: 'Zero Build Step',
44
+ text: 'No bundler, no compiler. Write EJS-style templates and run with Node. It just works.'
45
45
  }
46
46
  ]
47
47
  }));
48
48
 
49
- // About
50
49
  app.get('/about', asjs.page('about', {
51
50
  title: 'About — __APP_TITLE__',
52
- headline: 'About this project',
53
- description: 'This app is built with ASJS on top of Express. ASJS adds layouts, async page models, client-side transitions, and form helpers without a front-end build step.',
51
+ headline: 'About __APP_TITLE__',
52
+ description: 'Built with ASJS on top of Express. Layouts, async page models, client-side transitions, and a loading bar — without a front-end build step.',
54
53
  facts: [
55
54
  { label: 'Engine', value: 'ASJS __ASJS_VERSION__' },
56
55
  { label: 'Framework', value: 'Express' },
@@ -66,66 +65,49 @@ app.get('/about', asjs.page('about', {
66
65
  ]
67
66
  }));
68
67
 
69
- // Contact GET
70
- const contactInitialState = {
71
- title: 'Contact — __APP_TITLE__',
72
- headline: 'Get in touch',
73
- description: 'Send a message using the form below. This form is handled on the server and returns a validation state or a success state.',
74
- formValues: { name: '', email: '', message: '' },
75
- formErrors: {},
76
- submitted: false
77
- };
68
+ app.get('/form', asjs.page('form', function(req) {
69
+ var sent = req.query.sent === '1';
70
+ return {
71
+ title: 'Contact __APP_TITLE__',
72
+ headline: sent ? 'Message sent!' : 'Get in touch',
73
+ description: sent
74
+ ? 'Thank you for reaching out. We will get back to you soon.'
75
+ : 'Have a question or want to work together? Send us a message.',
76
+ submitted: sent,
77
+ values: {},
78
+ errors: {}
79
+ };
80
+ }));
78
81
 
79
- app.get('/contact', asjs.page('contact', contactInitialState));
82
+ app.post('/form', function(req, res) {
83
+ var body = req.body || {};
84
+ var name = (body.name || '').trim();
85
+ var email = (body.email || '').trim();
86
+ var subject = (body.subject || '').trim();
87
+ var message = (body.message || '').trim();
88
+ var errors = {};
80
89
 
81
- // Contact POST
82
- app.post('/contact', asjs.createPageRoute('contact', {}, async (req) => {
83
- const formValues = asjs.normalizeFields(req.body, ['name', 'email', 'message']);
84
- const formErrors = asjs.validateFields(formValues, {
85
- name: {
86
- required: 'Please enter your name.'
87
- },
88
- email: {
89
- required: 'Please enter your email address.',
90
- pattern: {
91
- value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
92
- message: 'Please enter a valid email address.'
93
- }
94
- },
95
- message: {
96
- required: 'Please write a message.',
97
- minLength: {
98
- value: 10,
99
- message: 'Please write at least 10 characters.'
100
- }
101
- }
102
- });
90
+ if (name.length < 2) errors.name = 'Name must be at least 2 characters.';
91
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = 'Please enter a valid email address.';
92
+ if (!subject) errors.subject = 'Please select a subject.';
93
+ if (message.length < 10) errors.message = 'Message must be at least 10 characters.';
103
94
 
104
- if (asjs.hasValidationErrors(formErrors)) {
105
- return {
106
- status: 422,
95
+ if (Object.keys(errors).length > 0) {
96
+ return asjs.render(res, 'form', {
107
97
  title: 'Contact — __APP_TITLE__',
108
98
  headline: 'Get in touch',
109
- description: 'A few fields need attention before the form can be sent.',
110
- formValues,
111
- formErrors,
112
- submitted: false
113
- };
99
+ description: 'Have a question or want to work together? Send us a message.',
100
+ submitted: false,
101
+ values: body,
102
+ errors: errors
103
+ });
114
104
  }
115
105
 
116
- return {
117
- title: 'Message sent — __APP_TITLE__',
118
- headline: 'Message received',
119
- description: 'Your message has been received. This is a starter demo — no real email was sent.',
120
- formValues: contactInitialState.formValues,
121
- formErrors: {},
122
- submitted: true,
123
- sentValues: formValues
124
- };
125
- }));
106
+ res.redirect('/form?sent=1');
107
+ });
126
108
 
127
109
  app.use(asjs.errors());
128
110
 
129
111
  app.listen(port, () => {
130
- console.log('__APP_TITLE__ is running at http://localhost:' + port);
112
+ console.log('__APP_TITLE__ running at http://localhost:' + port);
131
113
  });
@@ -23,6 +23,5 @@
23
23
  </ul>
24
24
  <div class="hero-actions" style="margin-top: 24px;">
25
25
  <a href="/" class="button button-primary" data-asjs-transition="fade">Back to home</a>
26
- <a href="/contact" class="button button-secondary" data-asjs-transition="fade">Contact</a>
27
26
  </div>
28
- </div>
27
+ </div>
@@ -0,0 +1,76 @@
1
+ <div class="page-intro">
2
+ <span class="section-label">Contact</span>
3
+ <h1><%= headline %></h1>
4
+ <p><%= description %></p>
5
+ </div>
6
+
7
+ <% if (submitted) { %>
8
+
9
+ <div class="status-panel status-panel--success">
10
+ <h3>Message received!</h3>
11
+ <p>Thanks for reaching out. We typically respond within one business day.</p>
12
+ <div class="hero-actions" style="margin-top: 20px;">
13
+ <a href="/form" class="button button-primary" data-asjs-transition="fade">Send another</a>
14
+ <a href="/" class="button button-secondary" data-asjs-transition="fade">Back to home</a>
15
+ </div>
16
+ </div>
17
+
18
+ <% } else { %>
19
+
20
+ <% if (Object.keys(errors).length > 0) { %>
21
+ <div class="status-panel status-panel--error" style="margin-bottom: 0;">
22
+ <h3>Please fix the errors below</h3>
23
+ <p>Some fields need your attention before the form can be submitted.</p>
24
+ </div>
25
+ <% } %>
26
+
27
+ <div class="section-card form-panel">
28
+ <span class="section-label">Send a message</span>
29
+ <h2>We'd love to hear from you</h2>
30
+ <p>Fill in the form and we'll get back to you as soon as possible.</p>
31
+
32
+ <form action="/form" method="POST" class="asjs-form" style="margin-top: 28px;">
33
+
34
+ <div class="field-grid">
35
+ <div class="field-group<% if (errors.name) { %> has-error<% } %>">
36
+ <label class="field-label" for="f-name">Full name</label>
37
+ <input id="f-name" type="text" name="name" value="<%= values.name || '' %>" placeholder="Your full name" autocomplete="name" required>
38
+ <% if (errors.name) { %><span class="field-error"><%= errors.name %></span><% } %>
39
+ </div>
40
+ <div class="field-group<% if (errors.email) { %> has-error<% } %>">
41
+ <label class="field-label" for="f-email">Email address</label>
42
+ <input id="f-email" type="email" name="email" value="<%= values.email || '' %>" placeholder="you@example.com" autocomplete="email" required>
43
+ <% if (errors.email) { %><span class="field-error"><%= errors.email %></span><% } %>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="field-group<% if (errors.subject) { %> has-error<% } %>">
48
+ <label class="field-label" for="f-subject">Subject</label>
49
+ <select id="f-subject" name="subject" class="field-select">
50
+ <option value="">Select a topic…</option>
51
+ <option value="general" <%= values.subject === 'general' ? 'selected' : '' %>>General enquiry</option>
52
+ <option value="project" <%= values.subject === 'project' ? 'selected' : '' %>>Project collaboration</option>
53
+ <option value="support" <%= values.subject === 'support' ? 'selected' : '' %>>Technical support</option>
54
+ <option value="other" <%= values.subject === 'other' ? 'selected' : '' %>>Other</option>
55
+ </select>
56
+ <% if (errors.subject) { %><span class="field-error"><%= errors.subject %></span><% } %>
57
+ </div>
58
+
59
+ <div class="field-group<% if (errors.message) { %> has-error<% } %>">
60
+ <label class="field-label" for="f-message">Message</label>
61
+ <textarea id="f-message" name="message" rows="6" placeholder="Write your message here…" required><%= values.message || '' %></textarea>
62
+ <% if (errors.message) { %><span class="field-error"><%= errors.message %></span><% } %>
63
+ </div>
64
+
65
+ <div class="form-actions">
66
+ <span class="form-hint">We typically respond within 24 hours.</span>
67
+ <button type="submit" class="button button-primary">
68
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z"/></svg>
69
+ Send message
70
+ </button>
71
+ </div>
72
+
73
+ </form>
74
+ </div>
75
+
76
+ <% } %>
@@ -4,7 +4,6 @@
4
4
  <p><%= description %></p>
5
5
  <div class="hero-actions">
6
6
  <a href="/about" class="button button-primary" data-asjs-transition="fade">Learn more</a>
7
- <a href="/contact" class="button button-secondary" data-asjs-transition="fade">Get in touch</a>
8
7
  </div>
9
8
  </div>
10
9
 
@@ -1,10 +1,11 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-theme="light">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title><%= title %></title>
7
7
  <%- asjs.clientTags({ preload: true, theme: true }) %>
8
+ <script>(function(){var t=localStorage.getItem('asjs-theme')||(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
8
9
  </head>
9
10
  <body<%- asjs.bodyAttrs() %>>
10
11
  <%- asjs.progressMarkup() %>
@@ -18,5 +19,30 @@
18
19
  <span>&copy; __YEAR__ __APP_TITLE__</span>
19
20
  </footer>
20
21
  </div>
22
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle light / dark">
23
+ <span class="toggle-icon toggle-icon--light">
24
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
25
+ </span>
26
+ <span class="toggle-icon toggle-icon--dark">
27
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
28
+ </span>
29
+ </button>
30
+ <script>
31
+ (function(){
32
+ var btn=document.getElementById('theme-toggle');
33
+ if(!btn)return;
34
+ function sync(t){
35
+ document.documentElement.setAttribute('data-theme',t);
36
+ btn.querySelector('.toggle-icon--light').style.display=t==='dark'?'flex':'none';
37
+ btn.querySelector('.toggle-icon--dark').style.display=t==='dark'?'none':'flex';
38
+ }
39
+ sync(document.documentElement.getAttribute('data-theme')||'light');
40
+ btn.addEventListener('click',function(){
41
+ var next=document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark';
42
+ localStorage.setItem('asjs-theme',next);
43
+ sync(next);
44
+ });
45
+ })();
46
+ </script>
21
47
  </body>
22
48
  </html>
@@ -1,53 +0,0 @@
1
- <% if (locals.submitted) { %>
2
- <div class="section-card status-panel--success" style="margin-top: 0;">
3
- <span class="section-label" style="background: #dcfce7; color: #15803d;">Sent</span>
4
- <h2 style="margin-top: 14px; letter-spacing: -0.04em;"><%= headline %></h2>
5
- <p><%= description %></p>
6
- <div class="render-summary-grid" style="margin-top: 20px;">
7
- <div class="render-item">
8
- <span>Name</span>
9
- <strong><%= sentValues.name %></strong>
10
- </div>
11
- <div class="render-item">
12
- <span>Email</span>
13
- <strong><%= sentValues.email %></strong>
14
- </div>
15
- </div>
16
- <div class="hero-actions" style="margin-top: 24px;">
17
- <a href="/" class="button button-primary" data-asjs-transition="fade">Back to home</a>
18
- <a href="/contact" class="button button-secondary" data-asjs-transition="fade">Send another</a>
19
- </div>
20
- </div>
21
- <% } else { %>
22
- <div class="page-intro">
23
- <span class="section-label">Contact</span>
24
- <h1><%= headline %></h1>
25
- <p><%= description %></p>
26
- </div>
27
-
28
- <div class="section-card form-panel">
29
- <form class="webas-form" method="POST" action="/contact">
30
- <div class="field-grid">
31
- <div class="field-group">
32
- <span>Name</span>
33
- <input type="text" name="name" value="<%= formValues.name %>" placeholder="Your name" autocomplete="name">
34
- <% if (formErrors.name) { %><span class="field-error"><%= formErrors.name %></span><% } %>
35
- </div>
36
- <div class="field-group">
37
- <span>Email</span>
38
- <input type="email" name="email" value="<%= formValues.email %>" placeholder="you@example.com" autocomplete="email">
39
- <% if (formErrors.email) { %><span class="field-error"><%= formErrors.email %></span><% } %>
40
- </div>
41
- </div>
42
- <div class="field-group">
43
- <span>Message</span>
44
- <textarea name="message" placeholder="Your message (at least 10 characters)"><%= formValues.message %></textarea>
45
- <% if (formErrors.message) { %><span class="field-error"><%= formErrors.message %></span><% } %>
46
- </div>
47
- <div class="form-actions">
48
- <button type="submit" class="button button-primary">Send message</button>
49
- <span>The form is validated on the server before the page re-renders.</span>
50
- </div>
51
- </form>
52
- </div>
53
- <% } %>