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:
5
packages/goodwrites-viewer/.gitignore
vendored
Normal file
5
packages/goodwrites-viewer/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
/node_modules/
|
||||
/*.logs
|
||||
/.yarn/
|
||||
/dist/
|
||||
37
packages/goodwrites-viewer/package.json
Normal file
37
packages/goodwrites-viewer/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@morten-olsen/goodwrites-viewer",
|
||||
"main": "./dist/index.js",
|
||||
"dependencies": {
|
||||
"@fontsource/merriweather": "^4.5.14",
|
||||
"@morten-olsen/goodwrites": "workspace:^",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||
"babel-loader": "^9.1.0",
|
||||
"css-loader": "^6.7.2",
|
||||
"express": "^4.18.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-pdf": "^6.2.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"styled-components": "^5.3.6",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-dev-middleware": "^6.0.1",
|
||||
"webpack-hot-middleware": "^2.25.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/html-webpack-plugin": "^3.2.6",
|
||||
"@types/loader-utils": "^2.0.3",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/react-pdf": "^5.7.4",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-middleware": "^5.3.0",
|
||||
"@types/webpack-hot-middleware": "^2.25.6"
|
||||
}
|
||||
}
|
||||
1
packages/goodwrites-viewer/src/index.ts
Normal file
1
packages/goodwrites-viewer/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './webpack';
|
||||
62
packages/goodwrites-viewer/src/ui/app.tsx
Normal file
62
packages/goodwrites-viewer/src/ui/app.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { FC } from 'react';
|
||||
import '@fontsource/merriweather/index.css';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { Block } from './components/block';
|
||||
import { useDoc } from './features/doc';
|
||||
import { Document, Page } from 'react-pdf/dist/esm/entry.webpack5';
|
||||
|
||||
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 {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
font-family: 'Merriweather', sans-serif;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #3498db;
|
||||
}
|
||||
`;
|
||||
|
||||
const Cover = styled.img`
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 3rem;
|
||||
line-height: 3.8rem;
|
||||
margin-bottom: 0px;
|
||||
`;
|
||||
|
||||
const App: FC = () => {
|
||||
const doc = useDoc();
|
||||
return (
|
||||
<div>
|
||||
<GlobalStyle />
|
||||
<Title>{doc.title}</Title>
|
||||
<Cover src={doc.cover} />
|
||||
<Document file={doc.pdf}>
|
||||
<Page pageNumber={1} renderTextLayer={false} />
|
||||
</Document>
|
||||
{doc.parts.map((part, index) => (
|
||||
<Block key={index} block={part} doc={doc} />
|
||||
))}
|
||||
<a href={(doc as any).pdf}>PDF {(doc as any).pdf}</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { App };
|
||||
105
packages/goodwrites-viewer/src/ui/components/block/index.tsx
Normal file
105
packages/goodwrites-viewer/src/ui/components/block/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { FC } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import styled from 'styled-components';
|
||||
import { DocumentResult } from '@morten-olsen/goodwrites';
|
||||
|
||||
type Block = DocumentResult['parts'][0];
|
||||
|
||||
interface BlockProps {
|
||||
block: Block;
|
||||
doc: DocumentResult;
|
||||
}
|
||||
|
||||
const getBreadcrumb = (block: Block, doc: DocumentResult) => {
|
||||
const path = [...block.path];
|
||||
const elements: any[] = [
|
||||
{
|
||||
title: '#',
|
||||
},
|
||||
];
|
||||
|
||||
path.reduce((acc, part) => {
|
||||
const parent = acc[part as keyof typeof acc] as any;
|
||||
elements.push({
|
||||
...parent,
|
||||
title: parent.title ?? part,
|
||||
});
|
||||
return parent.content;
|
||||
}, doc.original.content as any);
|
||||
|
||||
return elements.map((s: any) => s.title).join(' > ');
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin: 8px 0;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Note = styled.div`
|
||||
background-color: #efefef;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Content = styled.div<{
|
||||
level: number;
|
||||
border?: string;
|
||||
}>`
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 2.1rem;
|
||||
border: solid 5px ${(props) => props.border || '#fff'};
|
||||
padding: 8px 32px;
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
const Breacrumb = styled.div`
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
font-family: monospace;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
`;
|
||||
|
||||
const getBorderColor = (state: Block['state']) => {
|
||||
switch (state) {
|
||||
case 'first-draft':
|
||||
return '#3498db';
|
||||
case 'final-draft':
|
||||
return '#2ecc71';
|
||||
case 'revisions':
|
||||
return '#f1c40f';
|
||||
case 'placeholder':
|
||||
return '#e74c3c';
|
||||
}
|
||||
|
||||
return '#fff';
|
||||
};
|
||||
|
||||
const Block: FC<BlockProps> = ({ block, doc }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Breacrumb>{getBreadcrumb(block, doc)}</Breacrumb>
|
||||
{block.content && (
|
||||
<Content level={1} border={getBorderColor(block.state)}>
|
||||
<ReactMarkdown>{block.content}</ReactMarkdown>
|
||||
</Content>
|
||||
)}
|
||||
{block.notes && <Note>{block.notes}</Note>}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { Block };
|
||||
49
packages/goodwrites-viewer/src/ui/features/doc/index.tsx
Normal file
49
packages/goodwrites-viewer/src/ui/features/doc/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
FC,
|
||||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { DocumentResult } from '@morten-olsen/goodwrites';
|
||||
|
||||
type Document = DocumentResult;
|
||||
|
||||
declare var __DOC_LOCATION__: string;
|
||||
|
||||
type DocContextValue = {
|
||||
doc: Document;
|
||||
};
|
||||
|
||||
type DocProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const DocContext = createContext<DocContextValue>(null as any);
|
||||
|
||||
const DocProvider: FC<DocProviderProps> = ({ children }) => {
|
||||
const [doc, setDoc] = useState<Document>(
|
||||
require(__DOC_LOCATION__) as Document
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hot = (module as any).hot;
|
||||
|
||||
if (hot) {
|
||||
hot.accept('../../../../demo-article/index.yml', () => {
|
||||
const newDoc = require(__DOC_LOCATION__) as Document;
|
||||
setDoc(newDoc);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <DocContext.Provider value={{ doc }}>{children}</DocContext.Provider>;
|
||||
};
|
||||
|
||||
const useDoc = () => {
|
||||
const { doc } = useContext(DocContext);
|
||||
return doc;
|
||||
};
|
||||
|
||||
export { DocProvider, DocContext, useDoc };
|
||||
23
packages/goodwrites-viewer/src/ui/index.tsx
Normal file
23
packages/goodwrites-viewer/src/ui/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './app';
|
||||
import { DocProvider } from './features/doc';
|
||||
|
||||
const createRootElement = () => {
|
||||
const rootContainer = document.createElement('div');
|
||||
rootContainer.setAttribute('id', 'root');
|
||||
document.body.appendChild(rootContainer);
|
||||
return rootContainer;
|
||||
};
|
||||
|
||||
const rootElement = document.getElementById('root') || createRootElement();
|
||||
|
||||
const Root = () => {
|
||||
return (
|
||||
<DocProvider>
|
||||
<App />
|
||||
</DocProvider>
|
||||
);
|
||||
};
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
82
packages/goodwrites-viewer/src/webpack.ts
Normal file
82
packages/goodwrites-viewer/src/webpack.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import express from 'express';
|
||||
import webpack, { Configuration } from 'webpack';
|
||||
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||
import { join } from 'path';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import hotMiddleware from 'webpack-hot-middleware';
|
||||
import devMiddleware from 'webpack-dev-middleware';
|
||||
|
||||
type WebpackOptions = {
|
||||
dev?: boolean;
|
||||
documentLocation: string;
|
||||
};
|
||||
|
||||
const createWebpack = ({
|
||||
documentLocation,
|
||||
dev = false,
|
||||
}: WebpackOptions): Configuration => ({
|
||||
mode: dev ? 'development' : 'production',
|
||||
context: join(__dirname, 'ui'),
|
||||
entry: ['webpack-hot-middleware/client', join(__dirname, 'ui', 'index.js')],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: documentLocation,
|
||||
loader: require.resolve('@morten-olsen/goodwrites-webpack-loader'),
|
||||
},
|
||||
{
|
||||
test: /\.[jt]sx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
plugins: [require.resolve('react-refresh/babel')],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/i,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__DOC_LOCATION__: JSON.stringify(documentLocation),
|
||||
}),
|
||||
new ReactRefreshWebpackPlugin(),
|
||||
],
|
||||
});
|
||||
|
||||
const createServer = (options: WebpackOptions) => {
|
||||
const server = express();
|
||||
const webpackConfig = createWebpack(options);
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
server.use(devMiddleware(compiler));
|
||||
server.use(hotMiddleware(compiler));
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
export { createWebpack, createServer };
|
||||
10
packages/goodwrites-viewer/tsconfig.json
Normal file
10
packages/goodwrites-viewer/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user