Compare commits
12
Commits
046fa2189f
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb4ed2f2bd | ||
|
|
320badafd5 | ||
|
|
cdde5bc952 | ||
|
|
6db47427ad | ||
|
|
8ebbbadf98 | ||
|
|
0f58d2fb21 | ||
|
|
b8e28e3f1a | ||
|
|
ac6f9ff6a3 | ||
|
|
e9d4fe31b5 | ||
|
|
f546431e5f | ||
|
|
78897dbc50 | ||
|
|
4dddfb9dc2 |
@@ -0,0 +1,23 @@
|
||||
# Release Notes
|
||||
|
||||
## v4 — 2026-07-18
|
||||
|
||||
- Added a web app manifest (`manifest.json`) and a 512x512 icon, so the site
|
||||
can be installed as a desktop app from Edge/Chrome — opens in its own
|
||||
window instead of a browser tab, while still loading live from the server.
|
||||
|
||||
## v3 — 2026-07-16
|
||||
|
||||
- Added a favicon (clock + "H" mark), with a transparent icon set for
|
||||
browser tabs and an opaque apple-touch-icon for iOS.
|
||||
- New customer form now redirects to the customer list after saving,
|
||||
instead of the new customer's edit page.
|
||||
- Customer addresses in the customer list now link out to a Google Maps
|
||||
search (URL-encoded, opens in a new tab).
|
||||
|
||||
## v2 — 2026-07-16
|
||||
|
||||
- Added a "+ New Customer" button and a standalone `/customers/new`
|
||||
form, so a customer's contact info can be added without first
|
||||
creating a service order. Reuses the edit-customer template for both
|
||||
create and edit.
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the container image and tag it for the registry.
|
||||
# Usage: ./build-and-tag.sh # reads version.txt, increments, builds, pushes
|
||||
# Usage: ./build-and-tag.sh <tag> # explicit tag (e.g. ./build-and-tag.sh v6)
|
||||
set -e
|
||||
|
||||
IMAGE=hardings-service
|
||||
REGISTRY=registry.digitalsorcery.net
|
||||
VERSION_FILE="$(dirname "$0")/version.txt"
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
TAG="$1"
|
||||
else
|
||||
# Read current version, increment the numeric portion
|
||||
CURRENT=$(cat "$VERSION_FILE" | tr -d '[:space:]')
|
||||
NUM=$(echo "$CURRENT" | sed 's/v//')
|
||||
TAG="v$((NUM + 1))"
|
||||
fi
|
||||
|
||||
echo "Building ${IMAGE}:${TAG} ..."
|
||||
podman build -t "${IMAGE}:${TAG}" -f Containerfile .
|
||||
podman tag "${IMAGE}:${TAG}" "${REGISTRY}/${IMAGE}:${TAG}"
|
||||
podman push "${REGISTRY}/${IMAGE}:${TAG}"
|
||||
|
||||
# Update version.txt to match the tag we just pushed
|
||||
echo "$TAG" > "$VERSION_FILE"
|
||||
|
||||
echo "Done. ${REGISTRY}/${IMAGE}:${TAG} pushed."
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "hardingsclocks-atrex",
|
||||
"name": "hardings-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Repair tracker for Harding's Clocks",
|
||||
"private": true,
|
||||
|
||||
+17
-1
@@ -52,7 +52,8 @@ nav {
|
||||
background: var(--color-wood-dark);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -73,6 +74,21 @@ nav a:hover {
|
||||
color: var(--color-wood-light);
|
||||
}
|
||||
|
||||
/* Below ~430px (a phone in portrait, at this site's 150% base font size)
|
||||
the brand name alone can approach full width - drop it to its own line
|
||||
above the links instead of letting the flex row overflow the viewport. */
|
||||
@media (max-width: 430px) {
|
||||
nav {
|
||||
padding: 0.6rem 1rem;
|
||||
gap: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
nav a.brand {
|
||||
flex-basis: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Harding's Service",
|
||||
"short_name": "Harding's Service",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#F2E8D5",
|
||||
"theme_color": "#4A2810",
|
||||
"icons": [
|
||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
@@ -20,6 +20,36 @@ router.get('/', (req, res) => {
|
||||
res.render('customers/list.njk', { customers, q });
|
||||
});
|
||||
|
||||
router.get('/new', (req, res) => {
|
||||
res.render('customers/edit.njk', { customer: {} });
|
||||
});
|
||||
|
||||
router.post('/new', (req, res) => {
|
||||
const b = req.body as Record<string, string>;
|
||||
|
||||
const name = (b.name || '').trim();
|
||||
if (!name) return res.status(400).send('Name is required');
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO customers
|
||||
(name, phone, alt_phone, email, address_line1, address_line2, city, state, zip, internal_note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
name,
|
||||
formatPhone(b.phone),
|
||||
formatPhone(b.alt_phone),
|
||||
b.email || null,
|
||||
b.address_line1 || null,
|
||||
b.address_line2 || null,
|
||||
b.city || null,
|
||||
b.state || null,
|
||||
b.zip || null,
|
||||
b.internal_note || null
|
||||
);
|
||||
|
||||
res.redirect(303, '/customers');
|
||||
});
|
||||
|
||||
router.get('/:id/edit', (req, res) => {
|
||||
const customer = db.prepare('SELECT * FROM customers WHERE id = ?').get(req.params.id);
|
||||
if (!customer) return res.status(404).send('Customer not found');
|
||||
|
||||
+18
-2
@@ -3,6 +3,7 @@ import nunjucks from 'nunjucks';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { formatCents } from './money.js';
|
||||
import { db } from './db.js';
|
||||
import ordersRouter from './routes/orders.js';
|
||||
import customersRouter from './routes/customers.js';
|
||||
import clocksRouter from './routes/clocks.js';
|
||||
@@ -40,7 +41,22 @@ app.use((err: unknown, _req: express.Request, res: express.Response, _next: expr
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
|
||||
const HOST = '127.0.0.1';
|
||||
app.listen(PORT, HOST, () => {
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
console.log(`Harding's Clocks running at http://${HOST}:${PORT}`);
|
||||
});
|
||||
|
||||
function shutdown(signal: string) {
|
||||
console.log(`${signal} received, shutting down`);
|
||||
server.close((err) => {
|
||||
db.close();
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{% extends "layout.njk" %}
|
||||
{% block title %}Edit {{ customer.name }} — Harding's Clocks{% endblock %}
|
||||
{% block title %}{% if customer.id %}Edit {{ customer.name }}{% else %}New Customer{% endif %} — Harding's Clocks{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Edit Customer</h1>
|
||||
<h1>{% if customer.id %}Edit Customer{% else %}New Customer{% endif %}</h1>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/customers/{{ customer.id }}/edit" class="stacked-form">
|
||||
<form method="post" action="{% if customer.id %}/customers/{{ customer.id }}/edit{% else %}/customers/new{% endif %}" class="stacked-form">
|
||||
<fieldset>
|
||||
<legend>Customer</legend>
|
||||
<div class="grid-2">
|
||||
<label>Name <input type="text" name="name" value="{{ customer.name }}" required></label>
|
||||
<label>Name <input type="text" name="name" value="{{ customer.name or '' }}" required></label>
|
||||
<label>Phone <input type="text" name="phone" value="{{ customer.phone or '' }}"></label>
|
||||
<label>Alt phone <input type="text" name="alt_phone" value="{{ customer.alt_phone or '' }}"></label>
|
||||
<label>Email <input type="email" name="email" value="{{ customer.email or '' }}"></label>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Customers</h1>
|
||||
<div class="header-actions">
|
||||
<a class="button-link" href="/customers/new">+ New Customer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="search-form" method="get" action="/customers">
|
||||
@@ -29,7 +32,11 @@
|
||||
<td>{{ c.email or '' }}</td>
|
||||
<td>
|
||||
{%- if c.address_line1 -%}
|
||||
{{ c.address_line1 }}{% if c.city %}, {{ c.city }}{% endif %}{% if c.state %}, {{ c.state }}{% endif %} {{ c.zip or '' }}
|
||||
{% set addr = c.address_line1 %}
|
||||
{% if c.city %}{% set addr = addr + ', ' + c.city %}{% endif %}
|
||||
{% if c.state %}{% set addr = addr + ', ' + c.state %}{% endif %}
|
||||
{% if c.zip %}{% set addr = addr + ' ' + c.zip %}{% endif %}
|
||||
<a href="https://www.google.com/maps/search/?api=1&query={{ addr | urlencode }}" target="_blank" rel="noopener">{{ addr }}</a>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td><a class="button-link" href="/?customer_id={{ c.id }}">Orders</a></td>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<div>
|
||||
<h1>Jim Harding</h1>
|
||||
<p>Clock Repair | Phone: 434 660 2030</p>
|
||||
<p>236 Gartin Place, Madison Heights, VA 24572</p>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<p><strong>Invoice #{{ order.order_number }}</strong></p>
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Harding's Clocks{% endblock %}</title>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#4A2810">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<script src="/vendor/htmx/htmx.min.js" defer></script>
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
v6
|
||||
Reference in New Issue
Block a user