+ {{#if target_url}} + {{title}} + {{else}} + {{title}} + {{/if}} +
+ {{status}} +{{description}}
+ +diff --git a/public/css/projects.css b/public/css/projects.css index 439ddcb..8cdb184 100644 --- a/public/css/projects.css +++ b/public/css/projects.css @@ -14,6 +14,7 @@ .project-title { margin: 0; + margin-bottom: 1rem; font-size: 2.5rem; } @@ -64,3 +65,116 @@ border-top: 1px solid var(--border-medium, #eee); margin-top: 1.5rem; } + +.projects-intro { + margin-bottom: 2.5rem; + font-size: 1.1rem; + color: var(--text-main); + border-bottom: 1px solid var(--border-medium, #eee); + padding-bottom: 1.5rem; +} + +/* Grid Layout */ +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 2rem; +} + +/* Individual Project Card */ +.project-card { + background: var(--bg-tag, #fafafa); + border: 1px solid var(--border-medium, #ccc); + border-radius: 6px; + padding: 1.5rem; + display: flex; + flex-direction: column; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.project-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +/* Card Header (Title & Status Badge) */ +.project-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.project-header h2 { + margin: 0; + font-size: 1.4rem; + line-height: 1.2; +} + +.project-header a { + text-decoration: none; + color: var(--text-main); +} + +.project-header a:hover { + color: var(--accent-primary); +} + +/* Status Badges */ +.project-status { + font-family: "Roboto Mono", monospace; + font-size: 0.75rem; + font-weight: bold; + padding: 0.25rem 0.5rem; + border-radius: 4px; + text-transform: uppercase; + white-space: nowrap; +} + +.project-status.active { + background: rgba(46, 125, 50, 0.1); + color: #2e7d32; + border: 1px solid rgba(46, 125, 50, 0.2); +} + +.project-status.archived { + background: rgba(230, 81, 0, 0.1); + color: #e65100; + border: 1px solid rgba(230, 81, 0, 0.2); +} + +.project-status.dead { + background: rgba(198, 40, 40, 0.1); + color: #c62828; + border: 1px solid rgba(198, 40, 40, 0.2); +} + +.project-links { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: auto; +} + +.project-links a { + background: var(--bg-tag, #f0f0f0); + padding: 0.5rem 1rem; + border-radius: 6px; + text-decoration: none; + font-family: "Roboto Mono", monospace; + font-size: 0.85rem; + font-weight: bold; + color: var(--text-main); + border: 1px solid var(--border-medium, #ccc); + transition: all 0.2s ease; + text-align: center; + flex: 1 1 auto; /* Allows buttons to stretch and fill space evenly */ +} + +.project-links a:hover { + background: var(--accent-primary); + color: white; +} diff --git a/public/css/resume.css b/public/css/resume.css index 01e481b..abaca9b 100644 --- a/public/css/resume.css +++ b/public/css/resume.css @@ -69,10 +69,10 @@ color: black !important; /* Ensure black ink for printing */ } - /* Prevent Page Bleed */ .resume-paper { overflow: hidden; page-break-after: avoid; + padding: 0.25in !important; } } @@ -84,7 +84,8 @@ :root[data-view-type="paper"] body.resume-body { font-family: "Helvetica", "Arial", sans-serif; color: #333; - line-height: 1.2; + font-size: 9.5pt; + line-height: 1.15; } :root[data-view-type="web"] h1.resume-h1 { font-size: 2rem; @@ -96,14 +97,17 @@ text-align: center; text-transform: uppercase; margin: 0; - font-size: 24pt; + /* font-size: 24pt; */ + font-size: 16pt; + margin-bottom: 2px; } .resume-contact-bar { text-align: center; font-size: 10pt; border-bottom: 2px solid #444; padding-bottom: 5px; - margin-bottom: 15px; + /* margin-bottom: 15px; */ + padding-bottom: 2px; } :root[data-view-type="web"] h2.resume-h2 { font-size: 1.4rem; @@ -112,19 +116,25 @@ margin-top: 1.5rem; } :root[data-view-type="paper"] h2.resume-h2 { - font-size: 14pt; + /* font-size: 14pt; */ border-bottom: 1px solid #888; text-transform: uppercase; margin: 15px 0 5px 0; + font-size: 11pt; } .resume-entry { - margin-bottom: 10px; + /* margin-bottom: 10px; */ + margin-bottom: 4px; } .resume-entry-header { display: flex; justify-content: space-between; font-weight: bold; } +.resume-entry-header, .resume-sub-header { + align-items: baseline; + flex-wrap: nowrap; +} .resume-sub-header { display: flex; justify-content: space-between; @@ -132,11 +142,30 @@ font-size: 11pt; } ul.resume { - margin: 5px 0; - padding-left: 20px; + margin: 2px 0; + padding-left: 15px; + /* margin: 5px 0; */ + /* padding-left: 20px; */ +} + +ul.resume li { + margin-bottom: 1px; } :root[data-view-type="paper"] .resume-link { text-decoration: none; color: #333; /* Match your resume text color */ } +.resume-skills { + margin-bottom: 10px; + font-size: 11pt; +} +.resume-skill-category { + margin-bottom: 3px; +} + +.resume-summary { + margin: 2px 0; + font-size: 11pt; + /* margin: 5px 0; */ +} diff --git a/src/routes/projects.js b/src/routes/projects.js index ee7d90c..6c0274e 100644 --- a/src/routes/projects.js +++ b/src/routes/projects.js @@ -8,17 +8,16 @@ const presentation = require("./presentation"); const { meta } = require("../config/loader"); +const path = require("path"); +const fs = require("fs").promises; +const matter = require("gray-matter"); +const HttpError = require("../utils/HttpError"); + const construction = new ConstructionRoutes(); const html = new HtmlRoutes(); const markdown = new MarkdownRoutes(); const { node_env } = meta; -if (node_env === "production" || node_env === "testing") { - // construction.register("/newsletter", "Newsletter"); - construction.register("/projects", "Projects"); -} else { - markdown.register("/projects", "projects"); -} router.use("/projects/website-presentation", presentation); html.register("/games/word-guesser", "word-guesser"); @@ -28,6 +27,39 @@ markdown.register("/projects/telemetry", "projects/telemetry"); markdown.register("/projects/xmonad", "projects/xmonad"); +router.get("/projects", async (req, res, next) => { + try { + const projectsDir = path.join(__dirname, "../../content/pages/projects"); + const files = await fs.readdir(projectsDir); + + const projects = []; + + for (const file of files) { + if (!file.endsWith(".md")) continue; + + const filePath = path.join(projectsDir, file); + const fileContent = await fs.readFile(filePath, "utf-8"); + const { data } = matter(fileContent); + + projects.push({ + title: data.title, + status: data.published ? "Active" : "Archived", + status_class: data.published ? "active" : "archived", + description: data.description || "", + target_url: data.repository || `/${data.slug}`, + external: !!data.repository, + retrospective_url: `/${data.slug}`, + repository: data.repository, + }); + } + + res.renderWithBaseContext("pages/projects", { projects }); + } catch (err) { + req.log.error(err.stack); + next(new HttpError("Could not load projects", 500)); + } +}); + router.use(construction.getRouter()); router.use(html.getRouter()); router.use(markdown.getRouter()); diff --git a/src/routes/resume.js b/src/routes/resume.js index 45a335a..6c6ae2d 100644 --- a/src/routes/resume.js +++ b/src/routes/resume.js @@ -33,6 +33,7 @@ res.renderWithBaseContext("pages/resume", { ...resumeData, title: `Resume - ${resumeData.name}`, + viewType: isPaper ? "paper" : "web", showSidebar: !isPaper, showFooter: !isPaper, showHeader: !isPaper, diff --git a/src/views/pages/projects.handlebars b/src/views/pages/projects.handlebars new file mode 100644 index 0000000..f379259 --- /dev/null +++ b/src/views/pages/projects.handlebars @@ -0,0 +1,42 @@ +{{#section "styles"}} + +{{/section}} + +
This site hosts a collection of ongoing and completed technical work. Each project reflects deliberate design choices aligned with minimalism, control, and clarity.
+{{description}}
+ +{{summary}}
+