diff --git a/components/Footer.jsx b/components/Footer.jsx new file mode 100644 index 0000000..528fd38 --- /dev/null +++ b/components/Footer.jsx @@ -0,0 +1,9 @@ +import ReactMarkdown from 'react-markdown'; + +export default function Footer({ content }) { + return ( +
+ {content} +
+ ); +} diff --git a/components/Header.jsx b/components/Header.jsx new file mode 100644 index 0000000..2d021cd --- /dev/null +++ b/components/Header.jsx @@ -0,0 +1,17 @@ +import ReactMarkdown from 'react-markdown'; + +export default function Header({ title, description, pictureUrl, pictureText }) { + return ( +
+
+

{title}

+ {description} +
+ {pictureUrl && ( +
+ {pictureText +
+ )} +
+ ); +} diff --git a/lib/getPortfolioProps.js b/lib/getPortfolioProps.js new file mode 100644 index 0000000..94f8156 --- /dev/null +++ b/lib/getPortfolioProps.js @@ -0,0 +1,119 @@ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +export default function getPortfolioProps() { + const dataPath = path.join(process.cwd(), 'portfolio.json'); + let raw; + try { + raw = fs.readFileSync(dataPath, 'utf8'); + } catch (err) { + raw = fs.readFileSync(path.join(process.cwd(), 'portfolio.default.json'), 'utf8'); + } + const data = JSON.parse(raw); + + const md5 = data.gravatarEmail + ? crypto.createHash('md5').update(data.gravatarEmail).digest('hex') + : null; + + const getGravatar = () => (md5 ? `https://gravatar.com/avatar/${md5}?s=256` : ''); + + let headerPictureUrl = null; + if (data.headerPicture) { + headerPictureUrl = data.headerPicture.useGravatar + ? getGravatar() + : data.headerPicture.linkUrl; + } + + let faviconUrl = null; + let faviconType = ''; + if (data.favicon) { + faviconUrl = data.favicon.useGravatar ? getGravatar() : data.favicon.linkUrl; + const ext = faviconUrl.split('.').pop().toUpperCase(); + const map = { + ICO: 'image/x-icon', + GIF: 'image/gif', + PNG: 'image/png', + SVG: 'image/svg+xml', + }; + faviconType = map[ext] || ''; + } + + // Build the list of portfolio links. We keep each entry at the index + // specified by its optional `order` field to preserve any custom order. + const slots = []; + // `add` places an entry in the `slots` array at its ordered index when + // provided, otherwise it appends the entry to the end of the array. + const add = (order, entry) => { + if (order !== undefined && order !== null) { + slots[order] = entry; + } else { + slots.push(entry); + } + }; + + const p = data.portfolio || {}; + // Mapping of portfolio section keys to functions that create an entry object + // from the corresponding configuration. Each entry is later placed in the + // `slots` array based on its `order` field. + const builders = { + github: (cfg) => ({ + url: cfg.username ? `https://github.com/${cfg.username}` : null, + title: 'GitHub', + type: 'github', + }), + twitter: (cfg) => ({ + url: cfg.handle ? `https://twitter.com/${cfg.handle}` : null, + title: 'Twitter', + type: 'twitter', + }), + linkedIn: (cfg) => ({ + url: cfg.username ? `https://www.linkedin.com/in/${cfg.username}` : null, + title: 'LinkedIn', + type: 'linkedin', + }), + email: (cfg) => ({ + url: cfg.address ? `mailto:${cfg.address}` : null, + title: `Email ${cfg.address || 'me'}`, + type: 'email', + target: '_self', + }), + bitbucket: (cfg) => ({ + url: cfg.username ? `https://bitbucket.org/${cfg.username}` : null, + title: 'Bitbucket', + type: 'bitbucket', + }), + stackOverflow: (cfg) => ({ + url: cfg.id ? `https://stackoverflow.com/users/${cfg.id}` : null, + title: 'Stack Overflow', + type: 'stack-overflow', + }), + stackExchange: (cfg) => ({ + url: cfg.id ? `https://stackexchange.com/users/${cfg.id}` : null, + title: 'Stack Exchange', + type: 'stack-exchange', + }), + }; + + // Iterate through the portfolio configuration and build the resulting list + // of portfolio links using the helpers above. + Object.entries(p).forEach(([key, cfg]) => { + const build = builders[key]; + if (build) { + add(cfg.order, build(cfg)); + } + }); + + // Remove any empty slots resulting from sparse ordering. + const portfolioEntries = slots.filter(Boolean); + const description = data.description || ''; + + return { + data, + headerPictureUrl, + faviconUrl, + faviconType, + portfolioEntries, + description, + }; +} diff --git a/pages/index.js b/pages/index.js index 729ab2c..34b17cf 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,130 +1,15 @@ import Head from "next/head"; -import ReactMarkdown from "react-markdown"; import PortfolioEntries from "../components/PortfolioEntries"; +import Header from "../components/Header"; +import Footer from "../components/Footer"; +import getPortfolioProps from "../lib/getPortfolioProps"; const DEFAULT_GOOGLE_FONTS_URL = "//fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic|PT+Sans:400,700|PT+Sans+Narrow:400,700|Inconsolata:400"; export async function getStaticProps() { - const fs = require("fs"); - const path = require("path"); - const crypto = require("crypto"); - const dataPath = path.join(process.cwd(), "portfolio.json"); - let raw; - try { - raw = fs.readFileSync(dataPath, "utf8"); - } catch (err) { - raw = fs.readFileSync( - path.join(process.cwd(), "portfolio.default.json"), - "utf8", - ); - } - const data = JSON.parse(raw); - - const md5 = data.gravatarEmail - ? crypto.createHash("md5").update(data.gravatarEmail).digest("hex") - : null; - - const getGravatar = () => - md5 ? `https://gravatar.com/avatar/${md5}?s=256` : ""; - - let headerPictureUrl = null; - if (data.headerPicture) { - headerPictureUrl = data.headerPicture.useGravatar - ? getGravatar() - : data.headerPicture.linkUrl; - } - - let faviconUrl = null; - let faviconType = ""; - if (data.favicon) { - faviconUrl = data.favicon.useGravatar - ? getGravatar() - : data.favicon.linkUrl; - const ext = faviconUrl.split(".").pop().toUpperCase(); - const map = { - ICO: "image/x-icon", - GIF: "image/gif", - PNG: "image/png", - SVG: "image/svg+xml", - }; - faviconType = map[ext] || ""; - } - - const entries = []; - const assign = (order, obj) => { - if (order !== undefined && order !== null) { - entries[order] = obj; - } else { - entries.push(obj); - } - }; - const p = data.portfolio || {}; - if (p.github) - assign(p.github.order, { - url: p.github.username ? `https://github.com/${p.github.username}` : null, - title: "GitHub", - type: "github", - }); - if (p.twitter) - assign(p.twitter.order, { - url: p.twitter.handle ? `https://twitter.com/${p.twitter.handle}` : null, - title: "Twitter", - type: "twitter", - }); - if (p.linkedIn) - assign(p.linkedIn.order, { - url: p.linkedIn.username - ? `https://www.linkedin.com/in/${p.linkedIn.username}` - : null, - title: "LinkedIn", - type: "linkedin", - }); - if (p.email) - assign(p.email.order, { - url: p.email.address ? `mailto:${p.email.address}` : null, - title: `Email ${p.email.address || "me"}`, - type: "email", - target: "_self", - }); - if (p.bitbucket) - assign(p.bitbucket.order, { - url: p.bitbucket.username - ? `https://bitbucket.org/${p.bitbucket.username}` - : null, - title: "Bitbucket", - type: "bitbucket", - }); - if (p.stackOverflow) - assign(p.stackOverflow.order, { - url: p.stackOverflow.id - ? `https://stackoverflow.com/users/${p.stackOverflow.id}` - : null, - title: "Stack Overflow", - type: "stack-overflow", - }); - if (p.stackExchange) - assign(p.stackExchange.order, { - url: p.stackExchange.id - ? `https://stackexchange.com/users/${p.stackExchange.id}` - : null, - title: "Stack Exchange", - type: "stack-exchange", - }); - - const portfolioEntries = entries.filter(Boolean); - - const description = data.description || ""; - return { - props: { - data, - headerPictureUrl, - faviconUrl, - faviconType, - portfolioEntries, - description, - }, + props: getPortfolioProps(), }; } @@ -166,29 +51,16 @@ export default function Home({ )} -
-
-

{data.title}

- {description} -
- {headerPictureUrl && ( -
- {data.headerPicture?.pictureText -
- )} -
+
- {data.footer && ( -
- {data.footer} -
- )} + {data.footer &&