asjs-express 1.6.0 → 1.8.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.8.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,54 @@ 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
+ var formData = {
96
+ title: 'Contact — __APP_TITLE__',
97
+ headline: 'Get in touch',
98
+ description: 'Have a question or want to work together? Send us a message.',
99
+ values: body,
100
+ errors: errors,
101
+ submitted: false
102
+ };
103
+
104
+ if (Object.keys(errors).length > 0) {
105
+ return asjs.render(res, 'form', formData);
106
+ }
107
+
108
+ return asjs.render(res, 'form', Object.assign({}, formData, {
109
+ headline: 'Message sent!',
110
+ submitted: true,
111
+ values: {},
112
+ errors: {}
113
+ }));
114
+ });
115
+
64
116
  app.use(asjs.errors());
65
117
 
66
118
  app.listen(port, () => {
@@ -0,0 +1,111 @@
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="card shadow-sm" style="border-radius:var(--r-xl);border-color:var(--ok-border);background:var(--ok-bg);">
10
+ <div class="card-body p-4 p-md-5">
11
+ <div class="d-flex align-items-start gap-4">
12
+ <div style="width:52px;height:52px;flex-shrink:0;border-radius:50%;background:rgba(52,211,153,0.15);border:1px solid var(--ok-border);display:flex;align-items:center;justify-content:center;">
13
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--ok)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
14
+ </div>
15
+ <div>
16
+ <h5 class="fw-bold mb-1" style="color:var(--ok);">Message received!</h5>
17
+ <p class="mb-3" style="color:var(--text-2);font-size:0.92rem;">Thanks for reaching out. We typically respond within one business day.</p>
18
+ <a href="/form" class="button button-primary" data-asjs-transition="fade">Send another message</a>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <% } else { %>
25
+
26
+ <% if (Object.keys(errors).length > 0) { %>
27
+ <div class="alert alert-danger d-flex align-items-start gap-3 mb-3" role="alert" style="border-radius:var(--r-lg);">
28
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
29
+ <div>
30
+ <strong>Please fix the errors below</strong>
31
+ <div class="mt-1" style="font-size:0.85rem;">Some fields need attention before the form can be submitted.</div>
32
+ </div>
33
+ </div>
34
+ <% } %>
35
+
36
+ <div class="card shadow-sm" style="border-radius:var(--r-xl);">
37
+ <div class="card-body p-4 p-lg-5">
38
+
39
+ <div class="d-flex align-items-center gap-3 mb-4 pb-3" style="border-bottom:1px solid var(--border);">
40
+ <div style="width:40px;height:40px;border-radius:var(--r-md);background:var(--accent-soft);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
41
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
42
+ </div>
43
+ <div>
44
+ <h5 class="mb-0 fw-bold" style="letter-spacing:-0.02em;color:var(--text);">Send us a message</h5>
45
+ <p class="mb-0" style="font-size:0.8rem;color:var(--text-3);">We respond to every message within 24 hours</p>
46
+ </div>
47
+ </div>
48
+
49
+ <form action="/form" method="POST" data-asjs-form>
50
+ <div class="row g-4">
51
+
52
+ <div class="col-md-6">
53
+ <label class="form-label fw-semibold" for="f-name" style="font-size:0.84rem;color:var(--text-2);">Full name</label>
54
+ <input type="text" id="f-name" name="name"
55
+ class="form-control form-control-lg<% if(errors.name){ %> is-invalid<% } %>"
56
+ value="<%= values.name||'' %>"
57
+ placeholder="Jane Smith"
58
+ autocomplete="name" required>
59
+ <% if(errors.name){ %><div class="invalid-feedback fw-medium"><%= errors.name %></div><% } %>
60
+ </div>
61
+
62
+ <div class="col-md-6">
63
+ <label class="form-label fw-semibold" for="f-email" style="font-size:0.84rem;color:var(--text-2);">Email address</label>
64
+ <input type="email" id="f-email" name="email"
65
+ class="form-control form-control-lg<% if(errors.email){ %> is-invalid<% } %>"
66
+ value="<%= values.email||'' %>"
67
+ placeholder="jane@example.com"
68
+ autocomplete="email" required>
69
+ <% if(errors.email){ %><div class="invalid-feedback fw-medium"><%= errors.email %></div><% } %>
70
+ </div>
71
+
72
+ <div class="col-12">
73
+ <label class="form-label fw-semibold" for="f-subject" style="font-size:0.84rem;color:var(--text-2);">Subject</label>
74
+ <select id="f-subject" name="subject"
75
+ class="form-select form-select-lg<% if(errors.subject){ %> is-invalid<% } %>">
76
+ <option value="">Select a topic…</option>
77
+ <option value="general" <%= values.subject==='general' ? 'selected' : '' %>>General enquiry</option>
78
+ <option value="project" <%= values.subject==='project' ? 'selected' : '' %>>Project collaboration</option>
79
+ <option value="support" <%= values.subject==='support' ? 'selected' : '' %>>Technical support</option>
80
+ <option value="other" <%= values.subject==='other' ? 'selected' : '' %>>Other</option>
81
+ </select>
82
+ <% if(errors.subject){ %><div class="invalid-feedback fw-medium"><%= errors.subject %></div><% } %>
83
+ </div>
84
+
85
+ <div class="col-12">
86
+ <label class="form-label fw-semibold" for="f-message" style="font-size:0.84rem;color:var(--text-2);">Message</label>
87
+ <textarea id="f-message" name="message" rows="6"
88
+ class="form-control form-control-lg<% if(errors.message){ %> is-invalid<% } %>"
89
+ placeholder="Tell us what&#39;s on your mind…" required><%= values.message||'' %></textarea>
90
+ <% if(errors.message){ %><div class="invalid-feedback fw-medium"><%= errors.message %></div><% } %>
91
+ </div>
92
+
93
+ <div class="col-12">
94
+ <div class="d-flex align-items-center justify-content-between gap-3 flex-wrap pt-1" style="border-top:1px solid var(--border);padding-top:20px!important;">
95
+ <div style="font-size:0.82rem;color:var(--text-3);display:flex;align-items:center;gap:7px;">
96
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
97
+ Your data is never shared with third parties.
98
+ </div>
99
+ <button type="submit" class="button button-primary" style="height:44px;padding:0 24px;font-size:0.9rem;">
100
+ <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>
101
+ Send message
102
+ </button>
103
+ </div>
104
+ </div>
105
+
106
+ </div>
107
+ </form>
108
+ </div>
109
+ </div>
110
+
111
+ <% } %>
@@ -1,22 +1,51 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title><%= title %></title>
7
- <%- asjs.clientTags({ preload: true, theme: true }) %>
8
- </head>
9
- <body<%- asjs.bodyAttrs() %>>
10
- <%- asjs.progressMarkup() %>
11
- <div class="page-shell">
12
- <%- asjs.header() %>
13
- <main class="view-frame"<%- asjs.viewAttrs() %>>
14
- <%- body %>
15
- </main>
16
- <footer class="footer">
17
- <span>Built with <strong>ASJS __ASJS_VERSION__</strong> + Express</span>
18
- <span>&copy; __YEAR__ __APP_TITLE__</span>
19
- </footer>
20
- </div>
21
- </body>
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light" data-bs-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title %></title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
8
+ <%- asjs.clientTags({ preload: true, theme: true }) %>
9
+ <script>(function(){var t=localStorage.getItem('asjs-theme')||(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);document.documentElement.setAttribute('data-bs-theme',t);})();</script>
10
+ </head>
11
+ <body<%- asjs.bodyAttrs() %>>
12
+ <%- asjs.progressMarkup() %>
13
+ <div class="page-shell">
14
+ <%- asjs.header() %>
15
+ <main class="view-frame"<%- asjs.viewAttrs() %>>
16
+ <%- body %>
17
+ </main>
18
+ <footer class="footer">
19
+ <span>Built with <strong>ASJS __ASJS_VERSION__</strong> + Express</span>
20
+ <span>&copy; __YEAR__ __APP_TITLE__</span>
21
+ </footer>
22
+ </div>
23
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle light / dark">
24
+ <span class="toggle-icon toggle-icon--light" style="display:none">
25
+ <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>
26
+ </span>
27
+ <span class="toggle-icon toggle-icon--dark" style="display:none">
28
+ <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>
29
+ </span>
30
+ </button>
31
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
32
+ <script>
33
+ (function(){
34
+ var btn = document.getElementById('theme-toggle');
35
+ if (!btn) return;
36
+ function sync(t) {
37
+ document.documentElement.setAttribute('data-theme', t);
38
+ document.documentElement.setAttribute('data-bs-theme', t);
39
+ btn.querySelector('.toggle-icon--light').style.display = t === 'dark' ? 'flex' : 'none';
40
+ btn.querySelector('.toggle-icon--dark').style.display = t === 'light' ? 'flex' : 'none';
41
+ }
42
+ sync(document.documentElement.getAttribute('data-theme') || 'light');
43
+ btn.addEventListener('click', function() {
44
+ var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
45
+ localStorage.setItem('asjs-theme', next);
46
+ sync(next);
47
+ });
48
+ })();
49
+ </script>
50
+ </body>
22
51
  </html>