asjs-express 1.6.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.6.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": {
@@ -4,12 +4,16 @@ const { setupAsjs } = require('asjs-express');
4
4
  const app = express();
5
5
  const port = process.env.PORT || __PORT__;
6
6
 
7
+ app.use(express.urlencoded({ extended: false }));
8
+ app.use(express.json());
9
+
7
10
  const asjs = setupAsjs(app, {
8
11
  rootDir: __dirname,
9
12
  defaultLayout: 'layouts/main',
10
13
  navItems: [
11
14
  { href: '/', label: 'Home', activeMode: 'exact' },
12
- { href: '/about', label: 'About', activeMode: 'exact' }
15
+ { href: '/about', label: 'About', activeMode: 'exact' },
16
+ { href: '/form', label: 'Contact', activeMode: 'exact' }
13
17
  ],
14
18
  brand: {
15
19
  href: '/',
@@ -61,6 +65,47 @@ app.get('/about', asjs.page('about', {
61
65
  ]
62
66
  }));
63
67
 
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
+ }));
81
+
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 = {};
89
+
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.';
94
+
95
+ if (Object.keys(errors).length > 0) {
96
+ return asjs.render(res, 'form', {
97
+ title: 'Contact — __APP_TITLE__',
98
+ headline: 'Get in touch',
99
+ description: 'Have a question or want to work together? Send us a message.',
100
+ submitted: false,
101
+ values: body,
102
+ errors: errors
103
+ });
104
+ }
105
+
106
+ res.redirect('/form?sent=1');
107
+ });
108
+
64
109
  app.use(asjs.errors());
65
110
 
66
111
  app.listen(port, () => {
@@ -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
+ <% } %>
@@ -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>