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 && (
+
+

+
+ )}
+
+ );
+}
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.footer && (
-
- {data.footer}
-
- )}
+ {data.footer && }
>
);
}