feat(ui): ui-shell header with tool links, mobile side nav, Youreka logo
- header: Carbon ui-shell (cds-header) with the Youreka logo (from idid-enterprise-portal), Image Cropper / Fingerprint Agent nav items (aria-current on the active tool), the agent status tag on the wizard, and the operator chip + Sign out on desktop - mobile: cds-side-nav (logo above the items, both tools, a divider, then Sign out) toggled by the ui-shell menu button; account actions move out of the header below 1056px, the nav is hidden at desktop widths - the header is built once in server.js (appHeader) and injected into the landing, both tools and error pages — no per-page duplication - removed my duplicate menu-button toggle: ui-shell coordinates the menu button and side nav itself (the duplicate cancelled it out) - fixes: 'cds-header' was hidden by a selector-list typo (cds-header, cds-side-nav:not(:defined)); the collapsed side nav no longer captures taps Tools banner logo asset added (public/assets/youreka-logo.png).
This commit is contained in:
+61
-25
@@ -75,19 +75,7 @@ function errorPage(res, status, title, message, actions = {}) {
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
<nav class="nav">
|
||||
<div class="container">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a8 8 0 0 0-8 8c0 3.5 1 5.5 2 7"/><path d="M16 21a10 10 0 0 0 4-13 8 8 0 0 0-15.7-1"/><path d="M8 21a8 8 0 0 1-1-11 5 5 0 0 1 9.7 1.4A6 6 0 0 1 12 17"/><path d="M12 21a4 4 0 0 0 4-4 4 4 0 0 0-1.2-2.9"/></svg>
|
||||
</span>
|
||||
YOUreka <small>Tools</small>
|
||||
</a>
|
||||
<div class="nav-right">
|
||||
<a class="linklike" href="/auth/logout">Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
${appHeader(null, null)}
|
||||
<main class="container narrow center-v">
|
||||
<div class="card message-card">
|
||||
<div class="message-icon error">${ICONS[icon] || ICONS.alert}</div>
|
||||
@@ -118,6 +106,50 @@ function esc(value) {
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
/**
|
||||
* The application header and mobile side nav (Carbon ui-shell), shared by
|
||||
* every page. `session` is present when the operator is signed in; `active`
|
||||
* marks the current tool. Rendered server-side so pages need no client
|
||||
* session fetch and hold a single header definition.
|
||||
*/
|
||||
const appHeader = (session, active) => {
|
||||
const toolLink = (href, label, key) =>
|
||||
`<cds-header-nav-item href="${href}"${key === active ? ' aria-current="page"' : ''}>${label}</cds-header-nav-item>`;
|
||||
const sideLink = (href, label, key) =>
|
||||
`<cds-side-nav-link href="${href}"${key === active ? ' aria-current="page"' : ''}>${label}</cds-side-nav-link>`;
|
||||
const name = session ? (session.firstName || session.phone || '') : '';
|
||||
const right = session
|
||||
? `<span class="user-chip"><span class="avatar">${esc(name.trim().slice(0, 2).toUpperCase())}</span>${esc(name)}</span>
|
||||
<a class="linklike" href="/auth/logout">Sign out</a>`
|
||||
: '<a class="linklike" href="/auth/login">Sign in</a>';
|
||||
return `
|
||||
<cds-header aria-label="YOUreka Tools">
|
||||
<cds-header-menu-button button-label-active="Close menu" button-label-inactive="Open menu" id="header-menu-button"></cds-header-menu-button>
|
||||
<cds-header-name href="/">
|
||||
<span class="brand-logo"><img src="/assets/youreka-logo.png" alt="" width="20" height="20"></span>YOUreka<span class="brand-suffix"> Tools</span>
|
||||
</cds-header-name>
|
||||
<cds-header-nav>
|
||||
${toolLink('/cropper', 'Image Cropper', 'cropper')}
|
||||
${toolLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
|
||||
</cds-header-nav>
|
||||
${active === 'fingerprint' ? '<cds-tag id="agent-status" type="red" class="header-agent">Agent offline</cds-tag>' : ''}
|
||||
<div class="header-right">${right}</div>
|
||||
</cds-header>
|
||||
<cds-side-nav aria-label="Tools" id="side-nav">
|
||||
<div class="side-nav-brand">
|
||||
<img src="/assets/youreka-logo.png" alt="YOUreka" width="40" height="40">
|
||||
<span>YOUreka Tools</span>
|
||||
</div>
|
||||
<cds-side-nav-items>
|
||||
${sideLink('/cropper', 'Image Cropper', 'cropper')}
|
||||
${sideLink('/fingerprint', 'Fingerprint Agent', 'fingerprint')}
|
||||
<cds-side-nav-divider></cds-side-nav-divider>
|
||||
<cds-side-nav-link href="/auth/logout">Sign out</cds-side-nav-link>
|
||||
</cds-side-nav-items>
|
||||
</cds-side-nav>
|
||||
`;
|
||||
};
|
||||
|
||||
// ── app factory ──────────────────────────────────────────────────────────────
|
||||
export function createApp(overrides = {}) {
|
||||
const cfg = {
|
||||
@@ -300,7 +332,7 @@ export function createApp(overrides = {}) {
|
||||
* client never has to fetch it (and never flashes a loading state).
|
||||
* Per-user content — must not be cached.
|
||||
*/
|
||||
const renderPage = (res, file, session) => {
|
||||
const renderPage = (res, file, session, active) => {
|
||||
fs.readFile(path.join(__dirname, 'public', 'assets', file), 'utf8', (error, html) => {
|
||||
if (error) return errorPage(res, 500, 'Something went wrong', 'The page could not be loaded.');
|
||||
const data = session
|
||||
@@ -310,21 +342,24 @@ export function createApp(overrides = {}) {
|
||||
.set('Cache-Control', 'no-store')
|
||||
.set('Vary', 'Cookie')
|
||||
.type('html')
|
||||
.send(html.replace('</head>', ` <script>window.__SESSION__ = ${data};</script>\n</head>`));
|
||||
.send(
|
||||
html
|
||||
.replace('<!--app-header-->', appHeader(session, active))
|
||||
.replace('</head>', ` <script>window.__SESSION__ = ${data};</script>\n</head>`)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
app.get('/', (req, res, next) => {
|
||||
fs.readFile(path.join(__dirname, 'public', 'index.html'), 'utf8', (error, html) => {
|
||||
if (error) return next(error);
|
||||
const nav = req.session
|
||||
? `<span class="user-chip"><span class="avatar">${esc((req.session.firstName || req.session.phone || '?').trim().slice(0, 2).toUpperCase())}</span>${esc(req.session.firstName || req.session.phone || '')}</span><a class="linklike" href="/auth/logout">Sign out</a>`
|
||||
: '<a class="linklike" href="/auth/login">Sign in</a>';
|
||||
res
|
||||
.set('Cache-Control', 'no-store')
|
||||
.set('Vary', 'Cookie')
|
||||
.type('html')
|
||||
.send(html.replace('<!--session-nav-->', nav));
|
||||
.send(html.replace('<!--app-header-->', appHeader(req.session, null)));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,7 +398,7 @@ export function createApp(overrides = {}) {
|
||||
// ── wizard page (session required) ────────────────────────────────────────
|
||||
app.get('/fingerprint', requirePage, async (req, res, next) => {
|
||||
try {
|
||||
renderPage(res, 'fp/index.html', await sessionPayload(req, { withDomains: true }));
|
||||
renderPage(res, 'fp/index.html', await sessionPayload(req, { withDomains: true }), 'fingerprint');
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
@@ -399,11 +434,12 @@ export function createApp(overrides = {}) {
|
||||
next();
|
||||
};
|
||||
app.get('/cropper', requirePage, pageCropGate, (req, res, next) => {
|
||||
renderPage(res, 'cropper/index.html', {
|
||||
phone: req.session.phone,
|
||||
firstName: req.session.firstName,
|
||||
lastName: req.session.lastName,
|
||||
});
|
||||
renderPage(
|
||||
res,
|
||||
'cropper/index.html',
|
||||
{ phone: req.session.phone, firstName: req.session.firstName, lastName: req.session.lastName },
|
||||
'cropper'
|
||||
);
|
||||
});
|
||||
const cropperClients = cfg.cropperClients !== undefined ? cfg.cropperClients : buildCropperClients();
|
||||
if (cropperClients) {
|
||||
|
||||
Reference in New Issue
Block a user