feat: init

This commit is contained in:
Morten Olsen
2022-12-06 09:12:53 +01:00
commit 3f5e941446
115 changed files with 13148 additions and 0 deletions

6
webpage/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/out
/.next
/node_modules/
/*.logs
/.yarn/
/dist/

4
webpage/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/// <reference types="next" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

20
webpage/next.config.js Normal file
View File

@@ -0,0 +1,20 @@
const withPlugins = require("next-compose-plugins");
const { withImages } = require('./plugins/with-images');
const { withGoodwrites } = require('./plugins/with-goodwrites');
const { withMarkdown } = require('./plugins/with-markdown');
const nextConfig = {
poweredByHeader: false,
images: {
disableStaticImages: true
},
experimental: {
concurrentFeatures: true,
},
};
module.exports = withPlugins([
withImages,
withGoodwrites,
withMarkdown,
], nextConfig);

25
webpage/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@morten-olsen/personal-webpage",
"private": true,
"dependencies": {
"@fontsource/merriweather": "^4.5.14",
"@fontsource/pacifico": "^4.5.9",
"@morten-olsen/markdown-loader": "workspace:^",
"@morten-olsen/personal-webpage-articles": "workspace:^",
"@morten-olsen/personal-webpage-profile": "workspace:^",
"@react-three/fiber": "^8.9.1",
"chroma-js": "^2.4.2",
"next": "^12.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.4",
"styled-components": "^5.3.6",
"three": "^0.147.0"
},
"devDependencies": {
"@morten-olsen/goodwrites-webpack-loader": "workspace:^",
"@types/chroma-js": "^2.1.4",
"@types/three": "^0.146.0",
"next-compose-plugins": "^2.2.1"
}
}

View File

@@ -0,0 +1,31 @@
exports.withGoodwrites = (nextConfig = {}) => ({
...nextConfig,
webpack(config, options) {
const { isServer, dev } = options;
let outputPath = "";
if (isServer && dev) {
outputPath = "../";
} else if (isServer) {
outputPath = "../../";
}
config.module.rules.push({
test: /article.yml$/,
use: [
{
loader: require.resolve("@morten-olsen/goodwrites-webpack-loader"),
options: {
publicPath: `${
nextConfig.assetPrefix || nextConfig.basePath || ""
}/_next/static/assets/`,
outputPath: `${outputPath}static/assets/`,
},
},
],
});
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options);
}
return config;
},
});

View File

@@ -0,0 +1,46 @@
exports.withImages = (nextConfig = {}) => ({
...nextConfig,
webpack(config, options) {
nextConfig = Object.assign(
{ inlineImageLimit: false, assetPrefix: "" },
nextConfig
);
const { isServer, dev } = options;
let outputPath = "";
if (isServer && dev) {
outputPath = "../";
} else if (isServer) {
outputPath = "../../";
}
config.module.rules.push({
test: /\.(jpe?g|png|gif|ico|webp|jp2|url|svg)$/,
// Next.js already handles url() in css/sass/scss files
// issuer: /\.\w+(?<!(s?c|sa)ss)$/i, // commented out because of a bug with require.context load https://github.com/webpack/webpack/issues/9309
use: [
{
loader: require.resolve("url-loader"),
options: {
limit: nextConfig.inlineImageLimit,
fallback: require.resolve("file-loader"),
publicPath: `${
nextConfig.assetPrefix || nextConfig.basePath || ""
}/_next/static/images/`,
outputPath: `${outputPath}static/images/`,
name: "[name]-[hash].[ext]",
esModule: nextConfig.esModule || false,
},
},
],
});
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options);
}
return config;
},
});

View File

@@ -0,0 +1,18 @@
exports.withMarkdown = (nextConfig = {}) => ({
...nextConfig,
webpack(config, options) {
config.module.rules.push({
test: /\.md$/,
use: [
{
loader: require.resolve("@morten-olsen/markdown-loader"),
},
],
});
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options);
}
return config;
},
});

View File

@@ -0,0 +1,58 @@
import React from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import { Title1 } from 'typography';
import { getArticles } from '@morten-olsen/personal-webpage-articles/dist/index';
type Props = {
article: ReturnType<typeof getArticles>[number];
};
const Wrapper = styled.div`
height: 300px;
width: 300px;
position: relative;
margin: 10px;
`;
const Title = styled(Title1)`
background: ${({ theme }) => theme.colors.primary};
padding: 5px 0;
line-height: 40px;
`;
const MetaWrapper = styled.div`
position: absolute;
top: 0;
left: 10px;
`;
const AsideWrapper = styled.aside<{
image?: string;
}>`
background: ${({ theme }) => theme.colors.primary};
background-size: cover;
background-position: center;
${({ image }) => (image ? `background-image: url(${image});` : '')}
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;
const ArticlePreview: React.FC<Props> = ({ article }) => {
return (
<Link href={`/articles/${article.meta.slug}`}>
<Wrapper>
<AsideWrapper image={article.cover!} />
<MetaWrapper>
<Title>{article.title}</Title>
</MetaWrapper>
</Wrapper>
</Link>
);
};
export default ArticlePreview;

View File

@@ -0,0 +1,134 @@
import React, { Suspense, useMemo, useRef } from 'react';
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
import { Clock, Euler, Vector3 } from 'three';
import { TextureLoader } from 'three/src/loaders/TextureLoader';
import styled from 'styled-components';
const smoke = require('./smoke.png');
const Wrapper = styled.div`
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
`;
const FrameLimiter = () => {
const [clock] = React.useState(new Clock());
const [fps] = React.useState(10);
useFrame((state: any) => {
state.ready = false;
const timeUntilNextFrame = 1000 / fps - clock.getDelta();
setTimeout(() => {
state.ready = true;
state.invalidate();
}, Math.max(0, timeUntilNextFrame));
});
return <></>;
};
const Cloud = ({ texture }) => {
const ref = useRef<any>();
const width = window.innerWidth / 2;
const height = window.innerHeight / 2;
useFrame(() => {
if (!ref.current) {
return;
}
ref.current.rotation.z -= 0.003;
});
return (
<mesh
ref={ref}
rotation={new Euler(1.16, -0.12, Math.random() * Math.PI * 2)}
position={
new Vector3(
Math.random() * width - width / 2,
500,
Math.random() * height - height / 2
)
}
>
<planeBufferGeometry args={[500, 500]} />
<meshLambertMaterial
opacity={0.55}
args={[{ map: texture, transparent: true }]}
/>
</mesh>
);
};
const Clouds = () => {
const colorMap = useLoader(TextureLoader, smoke);
const items = useMemo(() => new Array(30).fill(undefined), []);
return (
<>
{items.map((_, i) => (
<Cloud key={i} texture={colorMap} />
))}
</>
);
};
const Scene = () => {
return (
<Canvas
onCreated={({ gl }) => {
gl.setClearColor('red');
}}
gl={{
antialias: false,
}}
camera={{
fov: 60,
aspect: 1,
near: 1,
far: 1000,
position: [0, 0, 1],
rotation: [1.16, -0.12, 0.27],
}}
>
<FrameLimiter />
<scene>
<perspectiveCamera
args={[60, window.innerWidth / window.innerHeight, 1, 1000]}
position={new Vector3(0, 0, 1)}
rotation={new Euler(1.16, -0.12, 0.27)}
/>
<ambientLight args={[0x555555]} />
<fogExp2 args={[0x03544e, 0.001]} />
<directionalLight args={[0xff8c19]} position={new Vector3(0, 0, 1)} />
<pointLight
args={[0xcc6600, 50, 450, 1.7]}
position={new Vector3(200, 300, 100)}
/>
<pointLight
args={[0xd8547e, 50, 450, 1.7]}
position={new Vector3(100, 300, 100)}
/>
<pointLight
args={[0x3677ac, 50, 450, 1.7]}
position={new Vector3(300, 300, 200)}
/>
<Suspense fallback={null}>
<Clouds />
</Suspense>
</scene>
</Canvas>
);
};
const Background = () => {
return (
<Wrapper suppressHydrationWarning={true}>
{typeof window !== 'undefined' && <Scene />}
</Wrapper>
);
};
export { Background };

View File

@@ -0,0 +1,32 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: yellow;
`;
type Props = {
className?: string;
};
const Background: React.FC<Props> = ({ className }) => {
const [background, setBackground] = useState<any>();
useEffect(() => {
import('./background').then(({ Background: HeroBackground }) => {
setBackground(<HeroBackground />);
});
}, []);
return (
<Wrapper suppressHydrationWarning className={className}>
{background || null}
</Wrapper>
);
};
export { Background };

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,43 @@
import React, { ReactNode, useMemo } from 'react';
import styled from 'styled-components';
import { createTheme } from 'theme/create';
import { ThemeProvider } from 'theme/provider';
const Wrapper = styled.div`
background: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.foreground};
min-height: 90%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
`;
const BackgroundWrapper = styled.div`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
`;
type Props = {
backgroundColor: string;
children: ReactNode;
background?: ReactNode;
};
const Sheet: React.FC<Props> = ({ background, children }) => {
const theme = useMemo(() => createTheme({}), []);
return (
<ThemeProvider theme={theme}>
<Wrapper>
{background && <BackgroundWrapper>{background}</BackgroundWrapper>}
{children}
</Wrapper>
</ThemeProvider>
);
};
export { Sheet };

10
webpage/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import {} from 'styled-components';
import { Theme } from './theme/theme'; // Import type from above file
declare module 'styled-components' {
export interface DefaultTheme extends Theme {} // extends the global DefaultTheme with our ThemeType.
}
declare module '*.png' {
const value: string;
export = value;
}

View File

@@ -0,0 +1,58 @@
import React, { useMemo } from 'react';
import { createGlobalStyle } from 'styled-components';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import '@fontsource/merriweather';
import '@fontsource/pacifico';
import { ThemeProvider } from '../theme/provider';
import { createTheme } from '../theme/create';
import chroma from 'chroma-js';
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
html {
font-size: 16px;
}
h1, h2, h3,h4, h5, h6 {
margin: 0;
}
html, body, body > #__next {
height: 100%;
}
body {
margin: 0;
padding: 0;
font-family: 'Merriweather', sans-serif;
background: ${({ theme }) => theme.colors.background};
}
a {
text-decoration: none;
}
`;
export default function App({ Component, pageProps }: AppProps) {
const theme = useMemo(
() =>
createTheme({
baseColor: chroma.random().brighten(1).hex(),
}),
[]
);
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Head>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<Component {...pageProps} />
</ThemeProvider>
);
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
class RootDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
static async getInitialProps(ctx: any) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => (props: any) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}
export default RootDocument;

View File

@@ -0,0 +1,176 @@
import { getArticles } from '@morten-olsen/personal-webpage-articles';
import { GetStaticPaths, GetStaticProps } from 'next';
import styled from 'styled-components';
import React from 'react';
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
import { Jumbo, Overline } from 'typography';
type Props = {
article: ReturnType<typeof getArticles>[number];
};
const Wrapper = styled.div``;
const ArticleWrapper = styled.article`
margin-right: 40%;
display: flex;
flex-direction: column;
align-items: flex-end;
background: ${({ theme }) => theme.colors.background};
min-height: 100vh;
@media only screen and (max-width: 700px) {
margin-right: 0;
}
`;
const ArticleContent = styled.div`
max-width: 900px;
padding: 50px;
letter-spacing: 0.5px;
line-height: 2.1rem;
color: ${({ theme }) => theme.colors.foreground};
img {
max-width: 100%;
}
p:first-of-type::first-letter {
font-size: 6rem;
float: left;
padding: 1rem;
margin: 0px 2rem;
font-weight: 100;
margin-left: 0rem;
}
p + p::first-letter {
margin-left: 1.8rem;
}
p {
margin-left: 50px;
@media only screen and (max-width: 700px) {
margin-left: 0;
}
}
a {
text-decoration: none;
color: ${({ theme }) => theme.colors.primary};
background: ${({ theme }) => theme.colors.foreground};
padding: 0.2rem 0.4rem;
}
h2,
h3,
h4,
h5,
h6 {
display: inline-block;
padding: 15px;
text-transform: uppercase;
margin: 5px 0;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
@media only screen and (max-width: 700px) {
background: transparent;
padding: 0;
}
}
`;
const AsideWrapper = styled.aside<{
image?: string;
}>`
width: 40%;
position: fixed;
right: 0;
top: 0;
bottom: 0;
background: ${({ theme }) => theme.colors.primary};
background-size: cover;
background-position: center;
opacity: 0.5;
${({ image }) => (image ? `background-image: url(${image});` : '')}
@media only screen and (max-width: 700px) {
position: static;
width: 100%;
opacity: 1;
height: 200px;
}
}
`;
const Title = styled(Jumbo)`
font-size: 60px;
line-height: 80px;
display: inline-block;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
padding: 0 15px;
text-transform: uppercase;
margin: 5px;
@media only screen and (max-width: 700px) {
font-size: 2rem;
line-height: 2.1rem;
}
`;
const Meta = styled(Overline)`
font-size: 0.8rem;
`;
const Article: React.FC<Props> = ({ article }) => {
return (
<Wrapper>
<ArticleWrapper>
<ArticleContent>
{article.title.split(' ').map((word, index) => (
<Title key={index}>{word}</Title>
))}
<div>
<Meta>
By Morten Olsen - 5 min read{' '}
{article.pdf && (
<a href={article.pdf} target="_blank">
download as PDF
</a>
)}
</Meta>
</div>
<AsideWrapper image={article.cover} />
<ReactMarkdown>{article.content}</ReactMarkdown>
</ArticleContent>
</ArticleWrapper>
</Wrapper>
);
};
const getStaticProps: GetStaticProps<Props> = async (context) => {
const { slug } = context.params;
const articles = getArticles();
const article = articles.find((a) => a.meta.slug === slug);
return {
props: {
article,
},
};
};
const getStaticPaths: GetStaticPaths = async () => {
const articles = getArticles();
return {
paths: articles.map((a) => ({
params: { slug: a.meta?.slug ?? a.title },
})),
fallback: false,
};
};
export { getStaticPaths, getStaticProps };
export default Article;

View File

@@ -0,0 +1,89 @@
import React, { FC } from 'react';
import styled from 'styled-components';
import { Jumbo } from 'typography';
import { getArticles } from '@morten-olsen/personal-webpage-articles/dist/index';
import { GetStaticProps } from 'next';
import { getPositions, Position } from '@morten-olsen/personal-webpage-profile';
import { Sheet } from '../components/sheet';
import ArticlePreview from 'components/articles/preview';
type Props = {
articles: ReturnType<typeof getArticles>;
positions: Position[];
};
const Hero = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Title = styled(Jumbo)`
font-size: 60px;
line-height: 80px;
display: inline-block;
background: ${({ theme }) => theme.colors.primary};
color: ${({ theme }) => theme.colors.foreground};
padding: 0 15px;
text-transform: uppercase;
margin: 5px;
@media only screen and (max-width: 700px) {
font-size: 2rem;
line-height: 2.1rem;
}
`;
const ArticleList = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 auto;
align-items: center;
justify-content: center;
`;
const Index: FC<Props> = ({ articles, positions }) => {
return (
<>
<Sheet backgroundColor="red">
<Hero>
{"Hi, I'm Morten Olsen".split(' ').map((char, index) => (
<Title key={index}>{char}</Title>
))}
</Hero>
<Hero>
{'And I make software'.split(' ').map((char, index) => (
<Title key={index}>{char}</Title>
))}
</Hero>
</Sheet>
<Sheet backgroundColor="#273c75">
<h2>Articles</h2>
<ArticleList>
{articles.map((article) => (
<ArticlePreview key={article.title} article={article} />
))}
</ArticleList>
</Sheet>
<Sheet backgroundColor="red">
{positions.map((position) => (
<div>{position.attributes.title}</div>
))}
</Sheet>
</>
);
};
export const getStaticProps: GetStaticProps<Props> = async () => {
const articles = getArticles();
const positions = getPositions();
console.log(articles);
return {
props: {
articles,
positions,
},
};
};
export default Index;

View File

@@ -0,0 +1,59 @@
import chroma from 'chroma-js';
import { Theme } from './theme';
const WHITE = chroma('white');
const BLACK = chroma('black');
type CreateOptions = {
baseColor?: string;
};
const isBright = (color: chroma.Color) => color.luminance() > 0.4;
const createTheme = (options: CreateOptions = {}) => {
const baseColor = options.baseColor
? chroma(options.baseColor)
: chroma.random();
const text = isBright(baseColor) ? BLACK : WHITE;
const bg = isBright(baseColor)
? baseColor.luminance(0.9)
: baseColor.luminance(0.005);
const theme: Theme = {
typography: {
Jumbo: {
weight: 'bold',
size: 2.8,
},
Title1: {
weight: 'bold',
},
Title2: {
weight: 'bold',
size: 1.3,
},
Body1: {},
Overline: {
size: 0.8,
upperCase: true,
},
Caption: {
size: 0.8,
},
Link: {
upperCase: true,
weight: 'bold',
},
},
colors: {
primary: baseColor.hex(),
foreground: text.hex(),
background: bg.hex(),
},
font: {
baseSize: 16,
},
};
return theme;
};
export { createTheme };

View File

@@ -0,0 +1,14 @@
import React, { ReactNode } from 'react';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
import { Theme } from './theme';
type Props = {
theme: Theme;
children: ReactNode;
};
const ThemeProvider: React.FC<Props> = ({ theme, children }) => (
<StyledThemeProvider theme={theme}>{children}</StyledThemeProvider>
);
export { ThemeProvider };

View File

@@ -0,0 +1,30 @@
type Typography = {
family?: string;
size?: number;
spacing?: number;
weight?: string;
upperCase?: boolean;
};
type Theme = {
typography: {
Jumbo: Typography;
Title2: Typography;
Title1: Typography;
Body1: Typography;
Caption: Typography;
Overline: Typography;
Link: Typography;
};
colors: {
primary: string;
foreground: string;
background: string;
};
font: {
baseSize: number;
family?: string;
};
};
export type { Theme, Typography };

View File

@@ -0,0 +1,57 @@
import styled from 'styled-components';
import { Theme, Typography } from '../theme/theme';
interface TextProps {
color?: keyof Theme['colors'];
bold?: boolean;
theme: Theme;
}
const BaseText = styled.span<TextProps>`
${({ theme }) =>
theme.font.family ? `font-family: ${theme.font.family};` : ''}
color: ${({ color, theme }) =>
color ? theme.colors[color] : theme.colors.foreground};
font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')};
font-size: ${({ theme }) => theme.font.baseSize}px;
`;
const get = (name: keyof Theme['typography'], theme: Theme): Typography => {
const typography = theme.typography[name];
return typography;
};
const createTypography = (name: keyof Theme['typography']) => {
const Component = styled(BaseText)<TextProps>`
font-size: ${({ theme }) =>
theme.font.baseSize * (get(name, theme).size || 1)}px;
font-weight: ${({ bold, theme }) =>
typeof bold !== 'undefined'
? 'bold'
: get(name, theme).weight || 'normal'};
${({ theme }) =>
get(name, theme).upperCase ? 'text-transform: uppercase;' : ''}
`;
return Component;
};
const Jumbo = createTypography('Jumbo');
const Title2 = createTypography('Title2');
const Title1 = createTypography('Title1');
const Body1 = createTypography('Body1');
const Overline = createTypography('Overline');
const Caption = createTypography('Caption');
const Link = createTypography('Link');
const types: { [key in keyof Theme['typography']]: typeof BaseText } = {
Jumbo,
Title2,
Title1,
Body1,
Overline,
Caption,
Link,
};
export type { TextProps };
export { types, Jumbo, Title2, Title1, Body1, Overline, Caption, Link };

33
webpage/tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noEmit": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": "./src",
"module": "esnext"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}