diff --git a/public/css/resume.css b/public/css/resume.css new file mode 100644 index 0000000..c7aee86 --- /dev/null +++ b/public/css/resume.css @@ -0,0 +1,98 @@ +.resume-pdf-layout { + background-color: #f0f0f0 !important; + min-height: 0 !important; /* Overrides global 100vh */ + height: auto !important; + margin: 0 !important; + display: flex; + justify-content: center; +} + +.resume-container { + max-width: none !important; /* Overrides global 800px */ + padding: 0 !important; + margin: 0 !important; +} + +@media screen { + body { + background: #e0e0e0; + display: flex; + justify-content: center; + padding: 20px; + } + .resume-paper { + background: white; + width: 8.5in; + min-height: 11in; + margin: 20px 0; + padding: 0.5in; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + box-sizing: border-box; + } +} + +@media print { + @page { + size: letter; + margin: 0; /* This removes the default browser margins where text is injected */ + } + .resume-pdf-layout { + background: white !important; + margin: 0.5in; /* Re-establish a visual margin for the actual content */ + padding: 0; + } + .resume-paper { + width: 8.5in; + height: 11in; + padding: 0; + margin: 0; + padding: 0.5in; /* Re-establish margins inside the paper for the printer */ + box-shadow: none; + } + .no-print { + display: none; + } +} + +body { + font-family: "Helvetica", "Arial", sans-serif; + color: #333; + line-height: 1.2; +} +h1 { + text-align: center; + text-transform: uppercase; + margin: 0; + font-size: 24pt; +} +.contact-bar { + text-align: center; + font-size: 10pt; + border-bottom: 2px solid #444; + padding-bottom: 5px; + margin-bottom: 15px; +} +h2 { + font-size: 14pt; + border-bottom: 1px solid #888; + text-transform: uppercase; + margin: 15px 0 5px 0; +} +.entry { + margin-bottom: 10px; +} +.entry-header { + display: flex; + justify-content: space-between; + font-weight: bold; +} +.sub-header { + display: flex; + justify-content: space-between; + font-style: italic; + font-size: 11pt; +} +ul { + margin: 5px 0; + padding-left: 20px; +} diff --git a/src/config/loader.js b/src/config/loader.js index d0f85b3..372a869 100644 --- a/src/config/loader.js +++ b/src/config/loader.js @@ -7,6 +7,10 @@ const domain = c?.network?.domain || process.env.SERVER_DOMAIN || "localhost"; const address = c?.network?.address || process.env.ADDRESS || "0.0.0.0"; const port = c?.network?.port || process.env.SERVER_PORT || 3400; + const logDir = c?.logging?.log_dir || process.env.LOG_DIR; + if (logDir == undefined) { + throw new Error("Log dir is undefined"); + } return { meta: { @@ -16,7 +20,7 @@ rootDir: c?.meta?.root_dir || process.env.ROOT_DIR, }, logging: { - logDir: c?.logging?.log_dir || process.env.LOG_DIR, + logDir, logLevel: c?.logging?.log_level || process.env.LOG_LEVEL || "info", logsDbPath: c?.logging?.db_path || process.env.LOGS_DB_PATH, }, @@ -67,12 +71,14 @@ if (!configPath) { console.info("Notice: No config file provided. Use --config "); console.info(" Using defaults"); - configPath = "../../config.toml"; + configPath = "config.toml"; let toml_config = {}; try { const raw = fs.readFileSync(path.resolve(configPath), "utf8"); const toml_config = parse(raw); - } finally { + return hydrate(toml_config); + } catch (e) { + console.warn("Warning: ", e.stack); return hydrate(toml_config); } } diff --git a/src/middleware/baseContext.js b/src/middleware/baseContext.js index 6840173..904e625 100644 --- a/src/middleware/baseContext.js +++ b/src/middleware/baseContext.js @@ -15,9 +15,34 @@ const getSiteTitle = (owner) => `${owner}'s Software Blog`; const POSTS_DIR = path.join(__dirname, "../../content/posts"); + +/** + * Merges CSS class and style overrides with default values. + * @param {Object} overrides - Object containing classes and styles to override. + * @returns {Object} The merged CSS configuration object. + */ +function cssOverride(overrides = {}) { + const defaults = { + classes: { + body: "pattern-dots", + layout: "layout", + sidebar: "sidebar", + container: "container", + }, + styles: {}, + }; + + return { + classes: { ...defaults.classes, ...(overrides.classes || {}) }, + styles: { ...defaults.styles, ...(overrides.styles || {}) }, + }; +} + const DEFAULT_CONTEXT = { showSidebar: true, showFooter: true, + showHeader: true, + css: cssOverride(), }; module.exports.attachBaseContextGetter = async (req, res, next) => { @@ -50,6 +75,29 @@ next(); }; +function renderWithBaseContext(res, baseContext) { + return (template, overrides = {}) => { + const context = Object.assign({}, baseContext, overrides); + res.render(template, context); + }; +} + +function renderWithCallback(res, baseContext) { + return (template, cb, overrides = {}) => { + let context = Object.assign({}, baseContext, overrides); + res.logger.info(cb); + context = cb(context); + res.render(template, context); + }; +} +function renderGenericMessage(res, baseContext) { + return (overrides = {}) => { + res.render( + "pages/generic-message", + Object.assign({}, baseContext, overrides), + ); + }; +} module.exports.buildBaseContext = async (req, res, next) => { const isAuthenticated = req.isAuthenticated; const token = generateToken(); @@ -60,17 +108,13 @@ }); res.locals.baseContext = baseContext; - res.renderWithBaseContext = (template, overrides = {}) => { - const context = Object.assign({}, baseContext, overrides); - res.render(template, context); - }; + res.renderWithBaseContext = renderWithBaseContext(res, baseContext); - res.renderGenericMessage = (overrides = {}) => { - res.render( - "pages/generic-message", - Object.assign({}, baseContext, overrides), - ); - }; + res.renderWithCallback = renderWithCallback(res, baseContext); + + res.renderGenericMessage = renderGenericMessage(res, baseContext); + + res.cssOverride = cssOverride; next(); }; diff --git a/src/middleware/hbs.js b/src/middleware/hbs.js index 140fce4..dbd56ea 100644 --- a/src/middleware/hbs.js +++ b/src/middleware/hbs.js @@ -77,7 +77,10 @@ registerHelpers(hbs); req.app.engine(VIEW_ENGINE, hbs.engine); req.app.set("view engine", VIEW_ENGINE); - req.app.set("views", path.join(__dirname, "../views")); + req.app.set("views", [ + path.join(__dirname, "../views"), + path.join(__dirname, "../../content/docs/hexascript"), + ]); } next(); diff --git a/src/routes/index.js b/src/routes/index.js index 1703b84..c3aeb11 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -9,6 +9,7 @@ const admin = require("./admin"); const tags = require("./tags"); const presentation = require("./presentation"); +const resume = require("./resume"); const contact = require("./contact"); const sitemap = require("./sitemap"); @@ -25,10 +26,19 @@ const faviconsPath = path.join(__dirname, "..", "..", "public", "favicons"); const faviconFile = path.resolve(faviconsPath, "favicon.ico"); +const hexascriptDocs = require("../../content/docs/hexascript/src/script.js"); + router.head("/health", (req, res) => { res.sendStatus(200); }); +const { winstonLogger } = require("../utils/logging"); +//winstonLogger.warn("Log?", hexascriptDocs()); +router.use("/hexa-docs", hexascriptDocs); +// router.use(hexascriptDocs); +// hexascriptDocs; +winstonLogger.info(hexascriptDocs); + router.get("/error/:code", errorPage); // Landing page after error is logged router.get("/error", errorPage); // Landing page after error is logged @@ -77,6 +87,7 @@ router.use(favicon(faviconFile)); router.use(contact, csrfToken); +router.use("/resume", resume); router.use(sitemap); router.use(pages); router.use(tags); diff --git a/src/routes/resume.js b/src/routes/resume.js new file mode 100644 index 0000000..aaa45b0 --- /dev/null +++ b/src/routes/resume.js @@ -0,0 +1,41 @@ +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs").promises; + +// Use your existing HttpError utility +const HttpError = require("../utils/HttpError"); + +/** + * Controller to render the Resume page + */ +router.get("/", async (req, res, next) => { + try { + // Path to your JSON data + const dataPath = path.resolve(__dirname, "../../content/resume.json"); + const fileContent = await fs.readFile(dataPath, "utf8"); + const resumeData = JSON.parse(fileContent); + + // Render using your existing Handlebars engine logic + // This follows the pattern in src/routes/post.js [cite: 25] + res.renderWithBaseContext("pages/resume", { + ...resumeData, + title: `Resume - ${resumeData.name}`, + showSidebar: false, + showFooter: false, + showHeader: false, + // css: res.cssOverride({ + // classes: { + // body: "resume-pdf-layout", + // layout: "resume-container", + // container: "resume-paper", + // }, + // }), + }); + } catch (err) { + req.log.error(err.stack); + next(new HttpError("Could not load resume data", 500)); + } +}); + +module.exports = router; diff --git a/src/utils/hbsHelpers.js b/src/utils/hbsHelpers.js index 789c063..6849f73 100644 --- a/src/utils/hbsHelpers.js +++ b/src/utils/hbsHelpers.js @@ -42,8 +42,17 @@ } } return null; - } + }, ); + hbs.handlebars.registerHelper("overrideCSS", function (options) { + const root = options.data.root; + const { target, prop, value } = options.hash; + + if (root.css && root.css[target]) { + root.css[target][prop] = value; + } + return ""; + }); } module.exports = { registerHelpers }; diff --git a/src/utils/processMenuLinks.js b/src/utils/processMenuLinks.js index 92386ad..545be2f 100644 --- a/src/utils/processMenuLinks.js +++ b/src/utils/processMenuLinks.js @@ -8,12 +8,18 @@ if (currentPath !== "/" && !item.href.endsWith(currentPath)) { item.href = item.href + currentPath; } + } else if (item.html) { + item.href = `/docs/hexa/${item.html}`; // fixme + } else if (item.frame) { + item.href = `/docs/hexa/${item.frame}`; // fixme + } else if (item.mermaid) { + item.href = `/docs/hexa/${item.mermaid}`; // fixme } if (item.submenu) { item.submenu = processMenuLinks( item.submenu, isAuthenticated, - currentPath + currentPath, ); if (!item.submenu.length) delete item.submenu; } diff --git a/src/utils/qualifyLinks.js b/src/utils/qualifyLinks.js index 846b23b..5334879 100644 --- a/src/utils/qualifyLinks.js +++ b/src/utils/qualifyLinks.js @@ -20,6 +20,24 @@ return qualified; }); } +const mapMenuTree = (links, transformFn) => { + return links.map((link) => { + const processed = transformFn({ ...link }); + if (processed.submenu) { + processed.submenu = mapMenuTree(processed.submenu, transformFn); + } + return processed; + }); +}; + +function qualifyNavLinks(links, baseUrl) { + return mapMenuTree(links, (link) => { + if (link.href) { + link.href = qualifyLink(link.href, baseUrl); + } + return link; + }); +} function qualifySitemapLinks(links) { return links.map((item) => { diff --git a/src/views/layouts/main.handlebars b/src/views/layouts/main.handlebars index d049ec5..28d2446 100644 --- a/src/views/layouts/main.handlebars +++ b/src/views/layouts/main.handlebars @@ -5,17 +5,19 @@ {{> site_headers }} - - {{> page_headers}} -
+ + {{#if showHeader}} + {{> page_headers}} + {{/if}} +
{{#if showSidebar}} -
diff --git a/src/views/layouts/page.handlebars b/src/views/layouts/page.handlebars new file mode 100644 index 0000000..d049ec5 --- /dev/null +++ b/src/views/layouts/page.handlebars @@ -0,0 +1,30 @@ + + + + + {{> site_headers }} + + + + {{> page_headers}} +
+ {{#if showSidebar}} + + {{/if}} +
+ {{{body}}} +
+
+ {{#if showFooter}} +
+ {{> footer}} +
+ {{/if}} + {{{_sections.scripts}}} + + + diff --git a/src/views/pages/resume.handlebars b/src/views/pages/resume.handlebars new file mode 100644 index 0000000..bc05b28 --- /dev/null +++ b/src/views/pages/resume.handlebars @@ -0,0 +1,44 @@ +{{overrideCSS target="classes" prop="body" value="resume-pdf-layout"}} +{{overrideCSS target="classes" prop="layout" value="resume-container"}} +{{overrideCSS target="classes" prop="container" value="resume-paper"}} + +{{#section "styles"}} + +{{/section}} + +

{{name}}

+
+ {{demographics.city}}, {{demographics.state}} {{demographics.zip}} | {{contact_information.cell}} | {{contact_information.email}} +
+ +
+

Education

+ {{#each education}} +
+
+ {{institution}} + {{from}} — {{to}} +
+
+ {{degree}} in {{program}} + {{city}}, {{state}} +
+
+ {{/each}} +
+ +
+

Experience

+ {{#each work_history}} +
+
+ {{position}} + {{from}} — {{to}} +
+
+ {{company}} + {{city}}, {{state}} +
+
+ {{/each}} +