mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
feat: init
This commit is contained in:
6
webpage/.gitignore
vendored
Normal file
6
webpage/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/out
|
||||
/.next
|
||||
/node_modules/
|
||||
/*.logs
|
||||
/.yarn/
|
||||
/dist/
|
||||
4
webpage/next-env.d.ts
vendored
Normal file
4
webpage/next-env.d.ts
vendored
Normal 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
20
webpage/next.config.js
Normal 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
25
webpage/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
31
webpage/plugins/with-goodwrites.js
Normal file
31
webpage/plugins/with-goodwrites.js
Normal 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;
|
||||
},
|
||||
});
|
||||
46
webpage/plugins/with-images.js
Normal file
46
webpage/plugins/with-images.js
Normal 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;
|
||||
},
|
||||
});
|
||||
|
||||
18
webpage/plugins/with-markdown.js
Normal file
18
webpage/plugins/with-markdown.js
Normal 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;
|
||||
},
|
||||
});
|
||||
58
webpage/src/components/articles/preview/index.tsx
Normal file
58
webpage/src/components/articles/preview/index.tsx
Normal 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;
|
||||
134
webpage/src/components/assets/background/background.tsx
Normal file
134
webpage/src/components/assets/background/background.tsx
Normal 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 };
|
||||
32
webpage/src/components/assets/background/index.tsx
Normal file
32
webpage/src/components/assets/background/index.tsx
Normal 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 };
|
||||
BIN
webpage/src/components/assets/background/smoke.png
Normal file
BIN
webpage/src/components/assets/background/smoke.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
43
webpage/src/components/sheet/index.tsx
Normal file
43
webpage/src/components/sheet/index.tsx
Normal 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
10
webpage/src/global.d.ts
vendored
Normal 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;
|
||||
}
|
||||
58
webpage/src/pages/_app.tsx
Normal file
58
webpage/src/pages/_app.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
webpage/src/pages/_document.tsx
Normal file
44
webpage/src/pages/_document.tsx
Normal 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;
|
||||
176
webpage/src/pages/articles/[slug].tsx
Normal file
176
webpage/src/pages/articles/[slug].tsx
Normal 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;
|
||||
89
webpage/src/pages/index.tsx
Normal file
89
webpage/src/pages/index.tsx
Normal 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;
|
||||
59
webpage/src/theme/create.ts
Normal file
59
webpage/src/theme/create.ts
Normal 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 };
|
||||
14
webpage/src/theme/provider.tsx
Normal file
14
webpage/src/theme/provider.tsx
Normal 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 };
|
||||
30
webpage/src/theme/theme.ts
Normal file
30
webpage/src/theme/theme.ts
Normal 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 };
|
||||
57
webpage/src/typography/index.tsx
Normal file
57
webpage/src/typography/index.tsx
Normal 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
33
webpage/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user