feat: added latex generation
11
.babelrc
@@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
"presets": ["next/babel"],
|
"presets": [
|
||||||
"plugins": [["styled-components", { "ssr": true }]]
|
["next/babel", {
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"babel-plugin-transform-typescript-metadata",
|
||||||
|
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||||
|
["styled-components", { "ssr": true }]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
35
config/plugins/withLatex/index.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const withLatex = (nextConfig = {}) => {
|
||||||
|
return Object.assign({}, nextConfig, {
|
||||||
|
webpack(config, options) {
|
||||||
|
const {isServer, dev} = options;
|
||||||
|
let outputPath = '';
|
||||||
|
if (isServer && dev) {
|
||||||
|
outputPath = "../";
|
||||||
|
} else if (isServer) {
|
||||||
|
outputPath = "../../";
|
||||||
|
}
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.tex.yml$/,
|
||||||
|
use: [{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
|
||||||
|
publicPath: `${nextConfig.assetPrefix || nextConfig.basePath || ''}/_next/static/images/`,
|
||||||
|
outputPath: `${outputPath}static/images/`,
|
||||||
|
name: "[name]-[hash].pdf",
|
||||||
|
esModule: nextConfig.esModule || false,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
loader: require.resolve('./webpack.js'),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
if (typeof nextConfig.webpack === "function") {
|
||||||
|
return nextConfig.webpack(config, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withLatex;
|
||||||
31
config/plugins/withLatex/webpack.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
require('reflect-metadata');
|
||||||
|
require('@babel/register')({
|
||||||
|
extensions: [".es6", ".es", ".jsx", ".js", ".mjs", ".ts"],
|
||||||
|
});
|
||||||
|
const latex = require("node-latex")
|
||||||
|
var Readable = require('stream').Readable
|
||||||
|
const { generateLatex } = require('../../../src/latex');
|
||||||
|
|
||||||
|
module.exports = function (source) {
|
||||||
|
var callback = this.async();
|
||||||
|
const location = this.resourcePath;
|
||||||
|
generateLatex(source, location)
|
||||||
|
.then((result) => {
|
||||||
|
const chunks = [];
|
||||||
|
const input = new Readable();
|
||||||
|
input.push(result);
|
||||||
|
input.push(null);
|
||||||
|
const latexStream = latex(input);
|
||||||
|
latexStream.on('data', (chunk) => {
|
||||||
|
chunks.push(Buffer.from(chunk));
|
||||||
|
})
|
||||||
|
latexStream.on('finish', () => {
|
||||||
|
const result = Buffer.concat(chunks);
|
||||||
|
callback(null, result);
|
||||||
|
})
|
||||||
|
latexStream.on('error', (err) => {
|
||||||
|
callback(err);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(callback);
|
||||||
|
}
|
||||||
3
content/articles/hiring/a4.tex.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
data:
|
||||||
|
structure: ./index.yml
|
||||||
|
generator: article
|
||||||
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@@ -1,5 +1,5 @@
|
|||||||
title: How to hire engineers, by an engineer
|
title: How to hire engineers, by an engineer
|
||||||
cover: cover.png
|
cover: cover.png
|
||||||
published: 2021-03-15
|
published: 2022-03-16
|
||||||
parts:
|
parts:
|
||||||
- main.md
|
- main.md
|
||||||
3
content/articles/hyperconnect/a4.tex.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
data:
|
||||||
|
structure: ./index.yml
|
||||||
|
generator: article
|
||||||
3
content/articles/my-home-runs-redux/a4.tex.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
data:
|
||||||
|
structure: ./index.yml
|
||||||
|
generator: article
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
BIN
content/articles/my-home-runs-redux/graph.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -1,5 +1,5 @@
|
|||||||
title: My home runs Redux
|
title: My home runs Redux
|
||||||
cover: cover.png
|
cover: cover.png
|
||||||
published: 2021-03-15
|
published: 2022-03-15
|
||||||
parts:
|
parts:
|
||||||
- main.md
|
- main.md
|
||||||
@@ -56,7 +56,7 @@ Now comes the part I have feared, where I need to draw a diagram.
|
|||||||
...sorry
|
...sorry
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
So this shows our final setup.
|
So this shows our final setup.
|
||||||
|
|
||||||
1
content/profile/a4.tex.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
generator: resume
|
||||||
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 559 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 558 KiB After Width: | Height: | Size: 558 KiB |
@@ -1,6 +1,7 @@
|
|||||||
const withPlugins = require("next-compose-plugins");
|
const withPlugins = require("next-compose-plugins");
|
||||||
|
|
||||||
const withImages = require("./config/plugins/withImages.js");
|
const withImages = require("./config/plugins/withImages.js");
|
||||||
|
const withLatex = require('./config/plugins/withLatex');
|
||||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
enabled: process.env.ANALYZE === "true",
|
enabled: process.env.ANALYZE === "true",
|
||||||
});
|
});
|
||||||
@@ -13,6 +14,7 @@ const nextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withPlugins([
|
module.exports = withPlugins([
|
||||||
|
withLatex,
|
||||||
[withImages,{
|
[withImages,{
|
||||||
esModule: true, // using ES modules is beneficial in the case of module concatenation and tree shaking.
|
esModule: true, // using ES modules is beneficial in the case of module concatenation and tree shaking.
|
||||||
inlineImageLimit: 0, // disable image inlining to data:base64
|
inlineImageLimit: 0, // disable image inlining to data:base64
|
||||||
|
|||||||
22
package.json
@@ -11,16 +11,25 @@
|
|||||||
"@fontsource/pacifico": "^4.5.3",
|
"@fontsource/pacifico": "^4.5.3",
|
||||||
"@react-three/drei": "^8.18.6",
|
"@react-three/drei": "^8.18.6",
|
||||||
"@react-three/fiber": "^7.0.26",
|
"@react-three/fiber": "^7.0.26",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
"next": "^12.1.0",
|
"next": "^12.1.0",
|
||||||
|
"node-latex": "^3.1.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-markdown": "^8.0.1",
|
"react-markdown": "^8.0.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
"rehype-img-size": "^1.0.1",
|
"rehype-img-size": "^1.0.1",
|
||||||
"remark-html": "^15.0.1",
|
"remark-html": "^15.0.1",
|
||||||
"styled-components": "^5.3.3",
|
"styled-components": "^5.3.3",
|
||||||
"three": "^0.138.3"
|
"three": "^0.138.3",
|
||||||
|
"typedi": "^0.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.8",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.17.8",
|
||||||
|
"@babel/plugin-transform-modules-commonjs": "^7.17.7",
|
||||||
|
"@babel/register": "^7.17.7",
|
||||||
"@next/bundle-analyzer": "^12.1.0",
|
"@next/bundle-analyzer": "^12.1.0",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/marked": "^4.0.3",
|
"@types/marked": "^4.0.3",
|
||||||
@@ -29,17 +38,24 @@
|
|||||||
"@types/three": "^0.138.0",
|
"@types/three": "^0.138.0",
|
||||||
"@types/yaml": "^1.9.7",
|
"@types/yaml": "^1.9.7",
|
||||||
"babel-plugin-styled-components": "^2.0.6",
|
"babel-plugin-styled-components": "^2.0.6",
|
||||||
|
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||||
|
"babel-register-esm": "^1.2.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"framer-motion": "^6.2.8",
|
"framer-motion": "^6.2.8",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
|
"html-entities": "^2.3.3",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.0.12",
|
||||||
"next-compose-plugins": "^2.2.1",
|
"next-compose-plugins": "^2.2.1",
|
||||||
"next-images": "^1.8.4",
|
"next-images": "^1.8.4",
|
||||||
"next-mdx-remote": "^4.0.0",
|
"next-mdx-remote": "^4.0.0",
|
||||||
"remark": "^14.0.2",
|
"os": "^0.1.2",
|
||||||
"remark-behead": "^3.0.0",
|
"reading-time": "^1.5.0",
|
||||||
|
"remark": "^13.0.0",
|
||||||
|
"remark-behead": "^2.3.3",
|
||||||
"remark-slug": "^7.0.1",
|
"remark-slug": "^7.0.1",
|
||||||
|
"ts-node": "^10.7.0",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "^4.6.2",
|
||||||
|
"unist-util-visit": "2.0.3",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"yaml": "^1.10.2",
|
"yaml": "^1.10.2",
|
||||||
"yarn": "^1.22.18"
|
"yarn": "^1.22.18"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Experience } from "../../data/experiences"
|
import { Experience } from "../../data/repos/experiences"
|
||||||
import { SlideIn } from '../animations/slide-in';
|
import { SlideIn } from '../animations/slide-in';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Profile } from '../../data/profile';
|
import { Profile } from '../../data/repos/profile';
|
||||||
import { HeroBackground } from './background';
|
import { HeroBackground } from './background';
|
||||||
import { SlideIn } from '../animations/slide-in';
|
import { SlideIn } from '../animations/slide-in';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Article } from '../../../data/articles';
|
import { Article } from '../../../data/repos/articles';
|
||||||
import { SlideIn } from '../../animations/slide-in';
|
import { SlideIn } from '../../animations/slide-in';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -33,12 +34,15 @@ const Header = styled.h3`
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Published = styled.time`
|
const Meta = styled.time`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Published = styled.time`
|
||||||
|
`;
|
||||||
|
|
||||||
const Image = styled.div<{
|
const Image = styled.div<{
|
||||||
src: string
|
src: string
|
||||||
}>`
|
}>`
|
||||||
@@ -56,10 +60,15 @@ const ArticleTile: React.FC<Props> = ({ article }) => (
|
|||||||
<Link href={`/articles/${article.id}`}>
|
<Link href={`/articles/${article.id}`}>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Inner>
|
<Inner>
|
||||||
|
<Meta>
|
||||||
|
<Published>{formatDistanceToNow(new Date(article.published || 0), { addSuffix: true })}</Published>
|
||||||
|
</Meta>
|
||||||
<Header>
|
<Header>
|
||||||
{article.title}
|
{article.title}
|
||||||
</Header>
|
</Header>
|
||||||
<Published>{article.published}</Published>
|
<Meta>
|
||||||
|
<br />{article.stats.minutes.toFixed(0)} min read
|
||||||
|
</Meta>
|
||||||
</Inner>
|
</Inner>
|
||||||
{article.cover && (
|
{article.cover && (
|
||||||
<ImageWrapper>
|
<ImageWrapper>
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
|
||||||
import yaml from 'yaml';
|
|
||||||
import { remark } from 'remark';
|
|
||||||
import remarkHtml from 'remark-html';
|
|
||||||
import behead from 'remark-behead';
|
|
||||||
import { visit } from 'unist-util-visit';
|
|
||||||
import { config } from '../config';
|
|
||||||
|
|
||||||
const imageModules = (require as any).context(
|
|
||||||
'../../articles',
|
|
||||||
true,
|
|
||||||
/\.(png|jpeg)$/,
|
|
||||||
)
|
|
||||||
const images = imageModules.keys().map((key) => ({
|
|
||||||
key,
|
|
||||||
url: imageModules(key).default,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const replaceImages = ({ id }) => (tree: any) => {
|
|
||||||
visit(tree, 'image', (node) => {
|
|
||||||
if (!node.url.startsWith('./')) return;
|
|
||||||
const correctedUrl = `.${path.resolve('/', id, node.url)}`;
|
|
||||||
const image = images.find((i: any) => i.key === correctedUrl);
|
|
||||||
node.url = image.url;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArticlePart = string | {
|
|
||||||
title?: string;
|
|
||||||
file?: string;
|
|
||||||
notes?: string;
|
|
||||||
parts?: ArticlePart[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Article = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
parts: ArticlePart[];
|
|
||||||
cover?: string;
|
|
||||||
summery?: string;
|
|
||||||
published?: string;
|
|
||||||
content: string;
|
|
||||||
html: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Articles {
|
|
||||||
#articles: Promise<Article[]>;
|
|
||||||
#images: typeof images;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.#images = images;
|
|
||||||
this.#articles = this.#load();
|
|
||||||
}
|
|
||||||
|
|
||||||
#loadArticle = async (location: string) => {
|
|
||||||
const id = path.basename(location);
|
|
||||||
const structureLocation = path.join(location, 'index.yml');
|
|
||||||
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
|
||||||
const structure = yaml.parse(structureContent) as Article;
|
|
||||||
|
|
||||||
const buildParts = async (part: ArticlePart, depth: number) => {
|
|
||||||
const content = [];
|
|
||||||
if (typeof part === 'string') {
|
|
||||||
part = {
|
|
||||||
file: part,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (part.title) {
|
|
||||||
content.push(
|
|
||||||
''.padStart(depth + 1, '#') + ' ' + part.title,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (part.file) {
|
|
||||||
const fileLocation = path.join(location, part.file);
|
|
||||||
const fileContent = await fs.readFile(fileLocation, 'utf-8');
|
|
||||||
content.push(
|
|
||||||
await remark().use(behead, { depth }).use(replaceImages, { id }).process(fileContent),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (part.parts) {
|
|
||||||
content.push(
|
|
||||||
...await Promise.all(
|
|
||||||
part.parts.map((part) => buildParts(part, depth + 1)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return content.join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const articleContent = await Promise.all(
|
|
||||||
structure.parts.map((part) => buildParts(part, 1)),
|
|
||||||
);
|
|
||||||
const cover = structure.cover
|
|
||||||
? this.getImage(id, structure.cover)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
|
|
||||||
const article: Article = {
|
|
||||||
...structure,
|
|
||||||
id,
|
|
||||||
cover,
|
|
||||||
content: articleContent.join('\n'),
|
|
||||||
html: String(await remark().use(remarkHtml).process(articleContent.join('\n'))),
|
|
||||||
};
|
|
||||||
|
|
||||||
return article;
|
|
||||||
};
|
|
||||||
|
|
||||||
#load = async () => {
|
|
||||||
const rootLocation = path.join(process.cwd(), 'articles');
|
|
||||||
const articleLocations = await fs.readdir(rootLocation);
|
|
||||||
const articles = await Promise.all(articleLocations.map(
|
|
||||||
(location) => this.#loadArticle(path.join(rootLocation, location)),
|
|
||||||
));
|
|
||||||
return articles
|
|
||||||
.sort((a, b) => new Date(b.published || 0).getTime() - new Date(a.published || 0).getTime())
|
|
||||||
.filter(a => a.published || config.dev)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public getImage = (article: string, name: string) => {
|
|
||||||
const url = `.${path.resolve('/', article, name)}`;
|
|
||||||
const image = this.#images.find((i: any) => i.key === url);
|
|
||||||
return image ? image.url : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get = async (id: string) => {
|
|
||||||
const articles = await this.list();
|
|
||||||
const article = articles.find(a => a.id === id);
|
|
||||||
return article;
|
|
||||||
};
|
|
||||||
|
|
||||||
public list = async () => {
|
|
||||||
const articles = await this.#articles;
|
|
||||||
return articles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const articleDB = new Articles();
|
|
||||||
30
src/data/assets/WebpackAssets.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { AssetResolver } from './';
|
||||||
|
|
||||||
|
const assetModules = (require as any).context(
|
||||||
|
'../../../content',
|
||||||
|
true,
|
||||||
|
/\.(png|jpe?g|svg|gif|tex\.yml)$/,
|
||||||
|
)
|
||||||
|
const assets = assetModules.keys().reduce((output, key: string) => ({
|
||||||
|
...output,
|
||||||
|
[path.resolve(
|
||||||
|
'/',
|
||||||
|
key,
|
||||||
|
)]: assetModules(key).default || assetModules(key),
|
||||||
|
}), {} as any);
|
||||||
|
|
||||||
|
class WebpackAssetResolver extends AssetResolver {
|
||||||
|
#assets = assets;
|
||||||
|
|
||||||
|
public getPath = (...location: string[]) => {
|
||||||
|
const target = path.resolve(
|
||||||
|
'/',
|
||||||
|
...location,
|
||||||
|
);
|
||||||
|
const assetModule = this.#assets[target];
|
||||||
|
return assetModule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WebpackAssetResolver };
|
||||||
8
src/data/assets/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Service } from "typedi";
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
abstract class AssetResolver {
|
||||||
|
public abstract getPath(...loc: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AssetResolver };
|
||||||
66
src/data/helpers/markdown.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import behead from 'remark-behead';
|
||||||
|
import remark from 'remark';
|
||||||
|
import visit from 'unist-util-visit';
|
||||||
|
import { AssetResolver } from '../assets';
|
||||||
|
|
||||||
|
type MarkdownSection = string | {
|
||||||
|
title?: string;
|
||||||
|
file?: string;
|
||||||
|
notes?: string;
|
||||||
|
parts?: MarkdownSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceImages = ({ location, assets }: any) => (tree: any) => {
|
||||||
|
visit(tree, 'image', (node: any) => {
|
||||||
|
if (!node.url.startsWith('./')) return;
|
||||||
|
const resolvedLocation = assets.getPath(
|
||||||
|
path.resolve('/', location, node.url),
|
||||||
|
);
|
||||||
|
node.url = resolvedLocation;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const generate = async (
|
||||||
|
location: string,
|
||||||
|
section: MarkdownSection,
|
||||||
|
depth: number,
|
||||||
|
assets: AssetResolver,
|
||||||
|
) => {
|
||||||
|
const result: string[] = [];
|
||||||
|
if (typeof section === 'string') {
|
||||||
|
section = {
|
||||||
|
file: section,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (section.title) {
|
||||||
|
result.push(''.padStart(depth, '#') + ' ' + section.title);
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
if (section.file) {
|
||||||
|
const fileLocation = path.resolve('content', location, section.file);
|
||||||
|
const fileContent = await fs.readFile(fileLocation, 'utf-8');
|
||||||
|
const markdown = String(
|
||||||
|
await remark()
|
||||||
|
.use(behead, { depth })
|
||||||
|
.use(replaceImages, { location, assets }).process(fileContent),
|
||||||
|
);
|
||||||
|
result.push(markdown);
|
||||||
|
}
|
||||||
|
if (section.parts) {
|
||||||
|
const sectionMarkdown = await Promise.all(
|
||||||
|
section.parts.map(s => generate(
|
||||||
|
location,
|
||||||
|
s,
|
||||||
|
depth + 1,
|
||||||
|
assets,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
result.push(...sectionMarkdown);
|
||||||
|
}
|
||||||
|
return result.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { MarkdownSection };
|
||||||
|
export { generate };
|
||||||
100
src/data/repos/articles.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import yaml from 'yaml';
|
||||||
|
import { config } from '../../config';
|
||||||
|
import { AssetResolver } from '../assets';
|
||||||
|
import { generate, MarkdownSection } from '../helpers/markdown';
|
||||||
|
import readingTime from 'reading-time';
|
||||||
|
|
||||||
|
export type ArticlePart = string | {
|
||||||
|
title?: string;
|
||||||
|
file?: string;
|
||||||
|
notes?: string;
|
||||||
|
parts?: ArticlePart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Article = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
parts: MarkdownSection[];
|
||||||
|
cover?: string;
|
||||||
|
summery?: string;
|
||||||
|
published?: string;
|
||||||
|
content: string;
|
||||||
|
stats: ReturnType<typeof readingTime>;
|
||||||
|
pdfs: {
|
||||||
|
a4: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ArticleDB {
|
||||||
|
#articles: Promise<Article[]>;
|
||||||
|
#assets: AssetResolver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
assets: AssetResolver,
|
||||||
|
) {
|
||||||
|
this.#assets = assets;
|
||||||
|
this.#articles = this.#load();
|
||||||
|
}
|
||||||
|
|
||||||
|
#loadArticle = async (location: string) => {
|
||||||
|
const id = path.basename(location);
|
||||||
|
const structureLocation = path.join('content', location, 'index.yml');
|
||||||
|
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
||||||
|
const structure = yaml.parse(structureContent) as Article;
|
||||||
|
|
||||||
|
const articleContent = await Promise.all(
|
||||||
|
structure.parts.map((part) => generate(
|
||||||
|
location,
|
||||||
|
part,
|
||||||
|
2,
|
||||||
|
this.#assets,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
const cover = structure.cover
|
||||||
|
? this.#assets.getPath(path.resolve('/', location, structure.cover))
|
||||||
|
: null;
|
||||||
|
const stats = readingTime(articleContent.join('\n'));
|
||||||
|
|
||||||
|
const article: Article = {
|
||||||
|
...structure,
|
||||||
|
id,
|
||||||
|
cover,
|
||||||
|
content: articleContent.join('\n'),
|
||||||
|
stats,
|
||||||
|
pdfs: {
|
||||||
|
a4: this.#assets.getPath(
|
||||||
|
path.resolve('/', 'articles', id, 'a4.tex.yml'),
|
||||||
|
) || null,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return article;
|
||||||
|
};
|
||||||
|
|
||||||
|
#load = async () => {
|
||||||
|
const rootLocation = path.join('content', 'articles');
|
||||||
|
const articleLocations = await fs.readdir(rootLocation);
|
||||||
|
const articles = await Promise.all(articleLocations.map(
|
||||||
|
(location) => this.#loadArticle(path.join('articles', location)),
|
||||||
|
));
|
||||||
|
return articles
|
||||||
|
.sort((a, b) => new Date(b.published || 0).getTime() - new Date(a.published || 0).getTime())
|
||||||
|
.filter(a => a.published || config.dev)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public get = async (id: string) => {
|
||||||
|
const articles = await this.list();
|
||||||
|
const article = articles.find(a => a.id === id);
|
||||||
|
return article;
|
||||||
|
};
|
||||||
|
|
||||||
|
public list = async () => {
|
||||||
|
const articles = await this.#articles;
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,9 @@
|
|||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
import { remark } from 'remark';
|
import { AssetResolver } from '../assets';
|
||||||
import remarkHtml from 'remark-html';
|
import { generate } from '../helpers/markdown';
|
||||||
import behead from 'remark-behead';
|
|
||||||
import { visit } from 'unist-util-visit';
|
|
||||||
|
|
||||||
const imageModules = (require as any).context(
|
|
||||||
'../../experiences',
|
|
||||||
true,
|
|
||||||
/\.(png|jpeg)$/,
|
|
||||||
)
|
|
||||||
const images = imageModules.keys().map((key) => ({
|
|
||||||
key,
|
|
||||||
url: imageModules(key).default,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const replaceImages = ({ id }) => (tree: any) => {
|
|
||||||
visit(tree, 'image', (node) => {
|
|
||||||
if (!node.url.startsWith('./')) return;
|
|
||||||
const correctedUrl = `.${path.resolve('/', id, node.url)}`;
|
|
||||||
const image = images.find((i: any) => i.key === correctedUrl);
|
|
||||||
node.url = image.url;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Experience = {
|
export type Experience = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,18 +11,20 @@ export type Experience = {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
title: string;
|
title: string;
|
||||||
|
content: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
content: string;
|
|
||||||
html: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class ExperienceDB {
|
export class ExperienceDB {
|
||||||
#experiences: Promise<Experience[]>;
|
#experiences: Promise<Experience[]>;
|
||||||
#images: typeof images;
|
#assets: AssetResolver;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
this.#images = images;
|
assets: AssetResolver,
|
||||||
|
) {
|
||||||
|
this.#assets = assets;
|
||||||
this.#experiences = this.#load();
|
this.#experiences = this.#load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,22 +34,24 @@ export class ExperienceDB {
|
|||||||
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
||||||
const structure = yaml.parse(structureContent) as Experience;
|
const structure = yaml.parse(structureContent) as Experience;
|
||||||
|
|
||||||
const fileLocation = path.join(location, 'description.md');
|
const content = await generate(
|
||||||
const fileContent = await fs.readFile(fileLocation, 'utf-8');
|
location,
|
||||||
const content = await remark().use(replaceImages, { id }).process(fileContent);
|
'description.md',
|
||||||
|
1,
|
||||||
|
this.#assets,
|
||||||
|
)
|
||||||
|
|
||||||
const experience: Experience = {
|
const experience: Experience = {
|
||||||
...structure,
|
...structure,
|
||||||
id,
|
id,
|
||||||
content: String(content),
|
content: String(content),
|
||||||
html: String(await remark().use(remarkHtml).process(content)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return experience;
|
return experience;
|
||||||
};
|
};
|
||||||
|
|
||||||
#load = async () => {
|
#load = async () => {
|
||||||
const rootLocation = path.join(process.cwd(), 'experiences');
|
const rootLocation = path.join(process.cwd(), 'content', 'experiences');
|
||||||
const articleLocations = await fs.readdir(rootLocation);
|
const articleLocations = await fs.readdir(rootLocation);
|
||||||
const articles = await Promise.all(articleLocations.map(
|
const articles = await Promise.all(articleLocations.map(
|
||||||
(location) => this.#loadExperience(path.join(rootLocation, location)),
|
(location) => this.#loadExperience(path.join(rootLocation, location)),
|
||||||
@@ -75,12 +59,6 @@ export class ExperienceDB {
|
|||||||
return articles.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
|
return articles.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getImage = (article: string, name: string) => {
|
|
||||||
const url = `.${path.resolve('/', article, name)}`;
|
|
||||||
const image = this.#images.find((i: any) => i.key === url);
|
|
||||||
return image ? image.url : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get = async (id: string) => {
|
public get = async (id: string) => {
|
||||||
const articles = await this.list();
|
const articles = await this.list();
|
||||||
const article = articles.find(a => a.id === id);
|
const article = articles.find(a => a.id === id);
|
||||||
@@ -92,5 +70,3 @@ export class ExperienceDB {
|
|||||||
return experiences;
|
return experiences;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const experienceDB = new ExperienceDB();
|
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Service } from 'typedi';
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
|
import { AssetResolver } from '../assets';
|
||||||
const imageModules = (require as any).context(
|
|
||||||
'../../profile',
|
|
||||||
true,
|
|
||||||
/\.(png|jpe?g|svg)$/,
|
|
||||||
)
|
|
||||||
const images = imageModules.keys().map((key) => ({
|
|
||||||
key,
|
|
||||||
url: imageModules(key).default,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export type Profile = {
|
export type Profile = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,27 +19,35 @@ export type Profile = {
|
|||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
export class ProfileDB {
|
export class ProfileDB {
|
||||||
#profile: Promise<Profile>;
|
#profile: Promise<Profile>;
|
||||||
|
#assets: AssetResolver;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
|
assets: AssetResolver,
|
||||||
|
) {
|
||||||
|
this.#assets = assets;
|
||||||
this.#profile = this.#load();
|
this.#profile = this.#load();
|
||||||
}
|
}
|
||||||
|
|
||||||
#load = async () => {
|
#load = async () => {
|
||||||
const rootLocation = path.join(process.cwd(), 'profile');
|
const rootLocation = path.join(process.cwd(), 'content', 'profile');
|
||||||
const structureLocation = path.join(rootLocation, 'index.yml');
|
const structureLocation = path.join(rootLocation, 'index.yml');
|
||||||
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
||||||
const structure = yaml.parse(structureContent) as Profile;
|
const structure = yaml.parse(structureContent) as Profile;
|
||||||
if (structure.avatar) {
|
if (structure.avatar) {
|
||||||
const image = images.find((i: any) => i.key === `.${path.resolve('/', structure.avatar)}`)
|
const image = this.#assets.getPath(
|
||||||
structure.avatar = image.url;
|
'profile',
|
||||||
|
structure.avatar,
|
||||||
|
)
|
||||||
|
structure.avatar = image || null;
|
||||||
}
|
}
|
||||||
structure.social = structure.social.map((social) => {
|
structure.social = structure.social.map((social) => {
|
||||||
const image = images.find((i: any) => i.key === `.${path.resolve('/', social.logo)}`)
|
const image = this.#assets.getPath('profile', social.logo);
|
||||||
return {
|
return {
|
||||||
...social,
|
...social,
|
||||||
logo: image?.url || null,
|
logo: image || null,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
return structure;
|
return structure;
|
||||||
@@ -58,5 +58,3 @@ export class ProfileDB {
|
|||||||
return profile;
|
return profile;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const profileDB = new ProfileDB();
|
|
||||||
3
src/latex/generators/Generator.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
type LatexGenerator<T = any> = (data: T, location: string) => Promise<string>;
|
||||||
|
|
||||||
|
export type { LatexGenerator };
|
||||||
45
src/latex/generators/article/index.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Article } from "../../../data/repos/articles";
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { LatexGenerator } from "../Generator";
|
||||||
|
import { generate } from '../../../data/helpers/markdown';
|
||||||
|
import path from 'path';
|
||||||
|
import yaml from 'yaml';
|
||||||
|
import { fromMarkdown } from "../../helpers/convert";
|
||||||
|
|
||||||
|
const assets = {
|
||||||
|
getPath: (source: string) => {
|
||||||
|
return source;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
type Data = {
|
||||||
|
structure: string;
|
||||||
|
}
|
||||||
|
const article: LatexGenerator<Data> = async (data, location) => {
|
||||||
|
const dir = path.dirname(location);
|
||||||
|
const structureLocation = path.resolve(dir, data.structure);
|
||||||
|
const structureContent = await fs.readFile(structureLocation, 'utf-8');
|
||||||
|
const structure = yaml.parse(structureContent) as Article;
|
||||||
|
const sections = await Promise.all(structure.parts.map(
|
||||||
|
part => generate(
|
||||||
|
dir,
|
||||||
|
part,
|
||||||
|
1,
|
||||||
|
assets,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
const content = sections.join('\n\n');
|
||||||
|
|
||||||
|
return `
|
||||||
|
\\documentclass[twocolumn]{article}
|
||||||
|
\\usepackage{graphicx}
|
||||||
|
\\usepackage[skip=5pt plus1pt, indent=10pt]{parskip}
|
||||||
|
\\setlength\\columnsep{1cm}
|
||||||
|
\\title{${structure.title}}
|
||||||
|
\\begin{document}
|
||||||
|
\\maketitle
|
||||||
|
${fromMarkdown(content, 0)}
|
||||||
|
\\end{document}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { article };
|
||||||
10
src/latex/generators/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { LatexGenerator } from "./Generator";
|
||||||
|
import { article } from './article';
|
||||||
|
import { resume } from './resume';
|
||||||
|
|
||||||
|
const generators: {[name: string]: LatexGenerator} = {
|
||||||
|
article,
|
||||||
|
resume,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { generators };
|
||||||
35
src/latex/generators/resume/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { LatexGenerator } from "../Generator";
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { AssetResolver } from '../../../data/assets';
|
||||||
|
import { ProfileDB } from '../../../data/repos/profile';
|
||||||
|
import { ExperienceDB } from '../../../data/repos/experiences';
|
||||||
|
|
||||||
|
const assets = {
|
||||||
|
getPath: (source: string) => {
|
||||||
|
return source;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
type Data = {
|
||||||
|
structure: string;
|
||||||
|
}
|
||||||
|
const resume: LatexGenerator<Data> = async (data, location) => {
|
||||||
|
Container.set(AssetResolver, assets);
|
||||||
|
const profileDB = Container.get(ProfileDB);
|
||||||
|
const experienceDB = Container.get(ExperienceDB);
|
||||||
|
|
||||||
|
const profile = await profileDB.get();
|
||||||
|
const experiences = await experienceDB.list();
|
||||||
|
|
||||||
|
return `
|
||||||
|
\\documentclass[twocolumn]{article}
|
||||||
|
\\usepackage{graphicx}
|
||||||
|
\\usepackage[skip=5pt plus1pt, indent=10pt]{parskip}
|
||||||
|
\\setlength\\columnsep{1cm}
|
||||||
|
\\title{${profile.name}}
|
||||||
|
\\begin{document}
|
||||||
|
\\maketitle
|
||||||
|
\\end{document}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { resume };
|
||||||
82
src/latex/helpers/convert.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { existsSync } from 'fs-extra';
|
||||||
|
import { decode } from 'html-entities';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
const latexTypes = [
|
||||||
|
'',
|
||||||
|
'section',
|
||||||
|
'subsection',
|
||||||
|
'paragraph',
|
||||||
|
];
|
||||||
|
|
||||||
|
const sanitize = (text?: string) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return decode(text)
|
||||||
|
.replace('&', '\\&')
|
||||||
|
.replace('_', '\\_')
|
||||||
|
.replace(/([^\\])\}/g, '$1\\}')
|
||||||
|
.replace(/([^\\])\{/g, '$1\\{')
|
||||||
|
.replace(/[^\\]\[/g, '\\[')
|
||||||
|
.replace('#', '\\#');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = (outerDepth: number) => ({
|
||||||
|
heading: (text: string, depth: number) => {
|
||||||
|
return `\\${latexTypes[outerDepth + depth]}{${sanitize(text)}}\n\n`
|
||||||
|
},
|
||||||
|
code: (input: string) => {
|
||||||
|
return `
|
||||||
|
\\begin{lstlisting}
|
||||||
|
${input}
|
||||||
|
\\end{lstlisting}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
text: (input: string) => {
|
||||||
|
return sanitize(input);
|
||||||
|
},
|
||||||
|
paragraph: (input: string) => {
|
||||||
|
return `${input}\n\n`
|
||||||
|
},
|
||||||
|
list: (input: string) => {
|
||||||
|
return `
|
||||||
|
\\begin{itemize}
|
||||||
|
${input}
|
||||||
|
\\end{itemize}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
listitem: (input: string) => {
|
||||||
|
return `\\item{${input}}`
|
||||||
|
},
|
||||||
|
link: (href: string, text: string) => {
|
||||||
|
if (!text || text === href) {
|
||||||
|
return `\\url{${sanitize(href)}}`;
|
||||||
|
}
|
||||||
|
return `${sanitize(text)} (\\url{${sanitize(href)}})`
|
||||||
|
},
|
||||||
|
strong: (text: string) => {
|
||||||
|
return `\\textbf{${sanitize(text)}}`
|
||||||
|
},
|
||||||
|
em: (text: string) => {
|
||||||
|
return `\\textbf{${sanitize(text)}}`
|
||||||
|
},
|
||||||
|
codespan: (code: string) => {
|
||||||
|
return `\\texttt{${sanitize(code)}}`
|
||||||
|
},
|
||||||
|
image: (link: string) => {
|
||||||
|
console.log('link', link);
|
||||||
|
if (!existsSync(link)) {
|
||||||
|
return 'Online image not supported';
|
||||||
|
}
|
||||||
|
return `\\begin{figure}[h!]
|
||||||
|
\\includegraphics[width=0.5\\textwidth]{${link}}
|
||||||
|
\\centering
|
||||||
|
\\end{figure}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const fromMarkdown = (md: string, depth: number) => {
|
||||||
|
marked.use({ renderer: renderer(depth) });
|
||||||
|
return marked.parse(md);
|
||||||
|
}
|
||||||
12
src/latex/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { generators } from "./generators";
|
||||||
|
import yaml from 'yaml';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const generateLatex = async (source: string, location: string) => {
|
||||||
|
const definition = yaml.parse(source);
|
||||||
|
const generator = generators[definition.generator];
|
||||||
|
const latex = await generator(definition.data, location);
|
||||||
|
return latex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generateLatex };
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
import Document from 'next/document';
|
import Document from 'next/document';
|
||||||
import { ServerStyleSheet } from 'styled-components';
|
import { ServerStyleSheet } from 'styled-components';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Article, articleDB } from '../../data/articles';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { Content } from '../../components/content';
|
import { Content } from '../../components/content';
|
||||||
import ReactMarkdown, { Components } from 'react-markdown';
|
import ReactMarkdown, { Components } from 'react-markdown';
|
||||||
import { Navigation } from '../../components/navigation';
|
import { Navigation } from '../../components/navigation';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { Profile, profileDB } from '../../data/profile';
|
import { Article, ArticleDB } from '../../data/repos/articles';
|
||||||
|
import { Profile, ProfileDB } from '../../data/repos/profile';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { AssetResolver } from '../../data/assets';
|
||||||
|
import { WebpackAssetResolver } from '../../data/assets/WebpackAssets';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -51,10 +55,13 @@ const Wrapper = styled.article`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Published = styled.time`
|
const Meta = styled.div`
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Published = styled.time`
|
||||||
|
`;
|
||||||
|
|
||||||
const Cover = styled.img`
|
const Cover = styled.img`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -82,7 +89,13 @@ const ArticleView: React.FC<Props> = ({
|
|||||||
<Content>
|
<Content>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<h1>{article.title}</h1>
|
<h1>{article.title}</h1>
|
||||||
<Published>3 days ago</Published>
|
<Meta>
|
||||||
|
<Published>{formatDistanceToNow(new Date(article.published || 0), { addSuffix: true })}</Published>
|
||||||
|
{' '} - {article.stats.minutes.toFixed(0)} minute read
|
||||||
|
{' '} - {article.pdfs.a4 &&(
|
||||||
|
<a href={article.pdfs.a4} target="_blank">Download as PDF</a>
|
||||||
|
)}
|
||||||
|
</Meta>
|
||||||
<Cover src={article.cover} />
|
<Cover src={article.cover} />
|
||||||
<ReactMarkdown components={components}>
|
<ReactMarkdown components={components}>
|
||||||
{article.content}
|
{article.content}
|
||||||
@@ -94,6 +107,8 @@ const ArticleView: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
Container.set(AssetResolver, new WebpackAssetResolver());
|
||||||
|
const articleDB = Container.get(ArticleDB);
|
||||||
const articles = await articleDB.list();
|
const articles = await articleDB.list();
|
||||||
return {
|
return {
|
||||||
paths: articles.map(a => `/articles/${a.id}`),
|
paths: articles.map(a => `/articles/${a.id}`),
|
||||||
@@ -103,6 +118,9 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
export async function getStaticProps({ params }: any) {
|
export async function getStaticProps({ params }: any) {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
Container.set(AssetResolver, new WebpackAssetResolver());
|
||||||
|
const articleDB = Container.get(ArticleDB);
|
||||||
|
const profileDB = Container.get(ProfileDB);
|
||||||
const article = await articleDB.get(id);
|
const article = await articleDB.get(id);
|
||||||
const profile = await profileDB.get();
|
const profile = await profileDB.get();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Container from 'typedi';
|
||||||
import { Content } from '../components/content';
|
import { Content } from '../components/content';
|
||||||
import { Experiences } from '../components/experiences';
|
import { Experiences } from '../components/experiences';
|
||||||
import { Featured } from '../components/featured';
|
import { Featured } from '../components/featured';
|
||||||
import { Hero } from '../components/hero';
|
import { Hero } from '../components/hero';
|
||||||
import { ArticleTile } from '../components/tiles/article';
|
import { ArticleTile } from '../components/tiles/article';
|
||||||
import { Article, articleDB } from '../data/articles';
|
import { AssetResolver } from '../data/assets';
|
||||||
import { Experience, experienceDB } from '../data/experiences';
|
import { WebpackAssetResolver } from '../data/assets/WebpackAssets';
|
||||||
import { Profile, profileDB } from '../data/profile';
|
import { Article, ArticleDB } from '../data/repos/articles';
|
||||||
|
import { Experience, ExperienceDB } from '../data/repos/experiences';
|
||||||
|
import { Profile, ProfileDB } from '../data/repos/profile';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
resume: string;
|
||||||
articles: Article[];
|
articles: Article[];
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
experiences: Experience[];
|
experiences: Experience[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const Home: React.FC<Props> = ({ articles, profile, experiences }) => (
|
const Home: React.FC<Props> = ({ resume, articles, profile, experiences }) => {
|
||||||
|
console.log('resume', resume);
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{profile.name} - {profile.tagline}</title>
|
<title>{profile.name} - {profile.tagline}</title>
|
||||||
@@ -32,13 +38,21 @@ const Home: React.FC<Props> = ({ articles, profile, experiences }) => (
|
|||||||
</Content>
|
</Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
|
Container.set(AssetResolver, new WebpackAssetResolver());
|
||||||
|
const articleDB = Container.get(ArticleDB);
|
||||||
|
const profileDB = Container.get(ProfileDB);
|
||||||
|
const experienceDB = Container.get(ExperienceDB);
|
||||||
|
const assets = Container.get(AssetResolver);
|
||||||
const articles = await articleDB.list();
|
const articles = await articleDB.list();
|
||||||
const profile = await profileDB.get();
|
const profile = await profileDB.get();
|
||||||
const experiences = await experienceDB.list();
|
const experiences = await experienceDB.list();
|
||||||
|
const resume = assets.getPath('profile', 'a4.tex.yml');
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
resume,
|
||||||
profile,
|
profile,
|
||||||
articles,
|
articles,
|
||||||
experiences,
|
experiences,
|
||||||
|
|||||||
@@ -10,14 +10,16 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"module": "esnext"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|||||||