Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ReactMarkdown from 'react-markdown';

export default function Footer({ content }) {
return (
<div className="footer">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
);
}
17 changes: 17 additions & 0 deletions components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ReactMarkdown from 'react-markdown';

export default function Header({ title, description, pictureUrl, pictureText }) {
return (
<div className="header">
<div className="header__text">
<h1 className="header__title">{title}</h1>
<ReactMarkdown>{description}</ReactMarkdown>
</div>
{pictureUrl && (
<div className="header__picture">
<img src={pictureUrl} alt={pictureText || ''} title={pictureText || ''} />
</div>
)}
</div>
);
}
119 changes: 119 additions & 0 deletions lib/getPortfolioProps.js
Original file line number Diff line number Diff line change
@@ -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,
};
}
150 changes: 11 additions & 139 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -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(),
};
}

Expand Down Expand Up @@ -166,29 +51,16 @@ export default function Home({
<style>{`:root{${styleVars.join(";")};}`}</style>
)}
</Head>
<div className="header">
<div className="header__text">
<h1 className="header__title">{data.title}</h1>
<ReactMarkdown>{description}</ReactMarkdown>
</div>
{headerPictureUrl && (
<div className="header__picture">
<img
src={headerPictureUrl}
alt={data.headerPicture?.pictureText || ""}
title={data.headerPicture?.pictureText || ""}
/>
</div>
)}
</div>
<Header
title={data.title}
description={description}
pictureUrl={headerPictureUrl}
pictureText={data.headerPicture?.pictureText}
/>
<div className="portfolio">
<PortfolioEntries entries={portfolioEntries} />
</div>
{data.footer && (
<div className="footer">
<ReactMarkdown>{data.footer}</ReactMarkdown>
</div>
)}
{data.footer && <Footer content={data.footer} />}
</>
);
}