mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
init
This commit is contained in:
9
content/templates/latex/article.tex
Normal file
9
content/templates/latex/article.tex
Normal file
@@ -0,0 +1,9 @@
|
||||
\documentclass{article}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{hyperref}
|
||||
\title{<%-article.title%>}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
\includegraphics[width=0.5\textwidth]{<%-article.cover%>}
|
||||
<%-article.body%>
|
||||
\end{document}
|
||||
149
content/templates/latex/resume.tex
Normal file
149
content/templates/latex/resume.tex
Normal file
@@ -0,0 +1,149 @@
|
||||
\documentclass[10pt, a4paper]{article}
|
||||
\usepackage[top=2cm, bottom=2cm, left=2cm, right=2cm]{geometry}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{calc}
|
||||
\usepackage{multicol}
|
||||
\usepackage{fancyhdr}
|
||||
|
||||
\setlength{\columnseprule}{0.1pt}
|
||||
%\setlength{\columnsep}{1.5cm}
|
||||
\def \columncount {2}
|
||||
\def \skillcolumncount {2}
|
||||
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\rhead{<%-profile.name%> \today}
|
||||
\lhead{Curriculum Vitae}
|
||||
\rfoot{Page \thepage}
|
||||
|
||||
\newenvironment{columns}{
|
||||
\ifnum\columncount>1
|
||||
\begin{multicols}{\columncount}
|
||||
\fi
|
||||
}{
|
||||
\ifnum\columncount>1
|
||||
\end{multicols}
|
||||
\fi
|
||||
\vspace{0.5cm}
|
||||
\hrule
|
||||
}
|
||||
|
||||
\newcommand{\cvinfo}[2]{
|
||||
\noindent \textbf{#1}\dotfill#2
|
||||
}
|
||||
|
||||
\newenvironment{cvtitle}[3]{
|
||||
\noindent\begin{minipage}{\textwidth}
|
||||
\noindent\begin{minipage}{\textwidth - 3.2cm}
|
||||
\Huge #1\newline\large #3
|
||||
\end{minipage}
|
||||
\noindent\begin{minipage}{3cm}
|
||||
\begin{flushright}
|
||||
\includegraphics[height=3cm]{#2}
|
||||
\end{flushright}
|
||||
\end{minipage}
|
||||
\vspace{0.5cm}
|
||||
\hrule
|
||||
\vspace{0.5cm}
|
||||
\ifnum\skillcolumncount>1
|
||||
\begin{multicols}{\skillcolumncount}
|
||||
\fi
|
||||
}{
|
||||
\ifnum\skillcolumncount>1
|
||||
\end{multicols}
|
||||
\fi
|
||||
\end{minipage}
|
||||
\hfill
|
||||
\begin{minipage}{\textwidth/3-2cm}
|
||||
\end{minipage}
|
||||
\vspace{1cm}
|
||||
\hrule
|
||||
}
|
||||
|
||||
\newenvironment{cvskills}{
|
||||
\noindent\begin{minipage}{\textwidth}
|
||||
\ifnum\skillcolumncount>1
|
||||
\begin{multicols}{\skillcolumncount}
|
||||
\fi
|
||||
}{
|
||||
\ifnum\skillcolumncount>1
|
||||
\end{multicols}
|
||||
\fi
|
||||
\vspace{0.5cm}
|
||||
\hrule
|
||||
\end{minipage}
|
||||
}
|
||||
|
||||
\newenvironment{cvbox}[3]
|
||||
{
|
||||
\noindent\begin{columns}
|
||||
\noindent{\Large \textbf{#1}} \hfill {\small #2} \\
|
||||
\textit{#3}
|
||||
\ifnum\columncount>2
|
||||
\vfill\null\columnbreak
|
||||
\else
|
||||
\\\\
|
||||
\fi
|
||||
}
|
||||
{
|
||||
\end{columns}
|
||||
%\end{minipage}
|
||||
\vspace{0.5cm}
|
||||
}
|
||||
|
||||
\newcommand{\cvskill}[2]{
|
||||
\textbf{#1}\dotfill
|
||||
\textit{#2}
|
||||
}
|
||||
|
||||
\newenvironment{cvexp}[4]
|
||||
{ \begin{cvbox}{#1}{#2 - #3}{#4} }
|
||||
{\end{cvbox}}
|
||||
|
||||
\newenvironment{cvproj}[3]
|
||||
{
|
||||
\noindent
|
||||
\begin{columns}
|
||||
\noindent{\Large \textbf{#1}} \\ {\small #3} \\
|
||||
{\tiny\textit{#2}}
|
||||
\ifnum\columncount>2
|
||||
\vfill\null\columnbreak
|
||||
\else
|
||||
\\\\
|
||||
\fi
|
||||
}
|
||||
{
|
||||
\end{columns}
|
||||
\vspace{0.5cm}
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{cvtitle}{<%-profile.name%>}{<%-profile.imagePath%>}{<%-profile.tagline%>}
|
||||
<% for (let info of profile.info) { %>
|
||||
\cvinfo{<%-info.name%>}{<%-info.value%>}
|
||||
<% } %>
|
||||
\end{cvtitle}
|
||||
|
||||
\begin{columns}
|
||||
\section*{Who am I?}
|
||||
<%-profile.about%>
|
||||
\end{columns}
|
||||
|
||||
\section*{Platform and languages}
|
||||
Platforms and languages which I have worked with. The list is a shortened down version\\\\
|
||||
\begin{cvskills}
|
||||
<% for (let skill of profile.skills.sort((a, b) => b.level - a.level)) { %>
|
||||
\cvskill{<%-skill.name%>}{<%-skill.level%>}
|
||||
<% } %>
|
||||
\end{cvskills}
|
||||
|
||||
\section*{Experience}
|
||||
<% for (let exp of positions.sort((a, b) => new Date(b.from) - new Date(a.from))) { %>
|
||||
\begin{cvexp}{<%-exp.company%>}{<%-exp.from%>}{<%-exp.to%>}{<%-exp.title%>}
|
||||
<%-exp.content%>
|
||||
\end{cvexp}
|
||||
<% } %>
|
||||
|
||||
\end{document}
|
||||
292
content/templates/react/article.tsx
Normal file
292
content/templates/react/article.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Jumbo } from "./typography";
|
||||
import { createTheme, ThemeProvider } from "./theme";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Page } from "types";
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* { box-sizing: border-box; }
|
||||
body, html { height: 100%; margin: 0; }
|
||||
body {
|
||||
font-size: 17px;
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
color: ${({ theme }) => theme.colors.foreground};
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Jumbo)`
|
||||
display: block;
|
||||
padding: 5rem 2rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
min-height: 100%;
|
||||
margin-bottom: 10rem;
|
||||
margin-top: 10rem;
|
||||
@media only screen and (max-width: 900px) {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ArticleTitleWord = styled(Jumbo)`
|
||||
font-size: 4rem;
|
||||
line-height: 4.1rem;
|
||||
display: inline-block;
|
||||
padding: 0 15px;
|
||||
text-transform: uppercase;
|
||||
margin: 10px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
color: ${({ theme }) => theme.colors.foreground};
|
||||
@media only screen and (max-width: 900px) {
|
||||
font-size: 2.5rem;
|
||||
line-height: 3.1rem;
|
||||
}
|
||||
@media only screen and (max-width: 700px) {
|
||||
padding: 5px;
|
||||
font-size: 2rem;
|
||||
line-height: 2.1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ArticleWrapper = styled.article`
|
||||
font-size: 1.1rem;
|
||||
font-family: "Merriweather", serif;
|
||||
|
||||
> p,
|
||||
ul,
|
||||
ol {
|
||||
letter-spacing: 0.08rem;
|
||||
line-height: 2.1rem;
|
||||
text-align: justify;
|
||||
max-width: 700px;
|
||||
margin: 2rem 4rem;
|
||||
list-style-position: inside;
|
||||
background: ${({ theme }) => theme.colors.background};
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
margin: 2rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
> p:first-of-type {
|
||||
display: block;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
> p:first-of-type::first-letter {
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
border: solid 5px ${({ theme }) => theme.colors.foreground};
|
||||
margin: 0 1rem 0 0;
|
||||
font-size: 6rem;
|
||||
float: left;
|
||||
clear: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 0.5rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> p + p::first-letter {
|
||||
margin-left: 1.8rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
max-width: 350px;
|
||||
margin-left: -100px;
|
||||
float: left;
|
||||
padding: 20px;
|
||||
padding-right: 40px;
|
||||
shape-outside: padding-box;
|
||||
position: relative;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
shape-outside: inherit;
|
||||
float: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
right: 20px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border: solid 5px ${({ theme }) => theme.colors.foreground};
|
||||
left: 0;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
width: 350px;
|
||||
font-size: 3rem;
|
||||
margin-left: -100px;
|
||||
float: left;
|
||||
padding: 50px;
|
||||
shape-outside: padding-box;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
color: ${({ theme }) => theme.colors.primary};
|
||||
content: "\\00BB";
|
||||
float: left;
|
||||
font-size: 6rem;
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
right: 20px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-right: 5px solid;
|
||||
border-color: ${({ theme }) => theme.colors.primary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin-right: 40%;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Side = styled.aside`
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
clip-path: polygon(40% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%);
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
clip-path: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Cover = styled.div<{ src: string }>`
|
||||
background: url(${({ src }) => src});
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Download = styled.a`
|
||||
display: inline-block;
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
color: ${({ theme }) => theme.colors.foreground};
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
const Author = styled.a`
|
||||
text-transform: uppercase;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
font-size: 2rem;
|
||||
margin: 1rem;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.colors.foreground};
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
border-bottom: solid 15px ${({ theme }) => theme.colors.primary};
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
bottom: 0px;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
}
|
||||
`;
|
||||
|
||||
const ArticlePage: Page<"article"> = ({ article, profile, pdfUrl }) => {
|
||||
return (
|
||||
<ThemeProvider theme={createTheme({ baseColor: article.color })}>
|
||||
<Helmet>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalStyle />
|
||||
<Wrapper>
|
||||
<Content>
|
||||
<Title>
|
||||
{article.title.split(" ").map((word, index) => (
|
||||
<ArticleTitleWord key={index}>{word}</ArticleTitleWord>
|
||||
))}
|
||||
<Author href="/">by {profile.name}</Author>
|
||||
</Title>
|
||||
<Download href={pdfUrl} target="_blank" rel="noreferrer">
|
||||
Download PDF
|
||||
</Download>
|
||||
<ArticleWrapper>
|
||||
<ReactMarkdown>{article.content}</ReactMarkdown>
|
||||
</ArticleWrapper>
|
||||
</Content>
|
||||
<Side>
|
||||
<Cover src={article.coverUrl} />
|
||||
</Side>
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticlePage;
|
||||
77
content/templates/react/components/article/grid/index.tsx
Normal file
77
content/templates/react/components/article/grid/index.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import ArticlePreview from "../preview";
|
||||
import { JumboArticlePreview } from "../preview/jumbo";
|
||||
import { MiniArticlePreview } from "../preview/mini";
|
||||
import { Article } from "types";
|
||||
|
||||
type Props = {
|
||||
articles: Article[];
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FeaturedArticle = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FeaturedArticles = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const RemainingArticles = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ArticleGrid: React.FC<Props> = ({ articles }) => {
|
||||
const sorted = useMemo(
|
||||
() => articles,
|
||||
// TODO:
|
||||
// articles.sort(
|
||||
// (a, b) =>
|
||||
// new Date(b.published).getTime() -
|
||||
// new Date(a.published).getTime()
|
||||
// ),
|
||||
[articles]
|
||||
);
|
||||
const featured1 = useMemo(() => sorted.slice(0, 1)[0], [sorted]);
|
||||
|
||||
const featured2 = useMemo(() => sorted.slice(1, 4), [sorted]);
|
||||
|
||||
const remaining = useMemo(() => sorted.slice(4, 12), [sorted]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<FeaturedArticle>
|
||||
<JumboArticlePreview article={featured1} />
|
||||
</FeaturedArticle>
|
||||
<FeaturedArticles>
|
||||
{featured2.map((article) => (
|
||||
<ArticlePreview key={article.title} article={article} />
|
||||
))}
|
||||
</FeaturedArticles>
|
||||
<RemainingArticles>
|
||||
{remaining.map((article) => (
|
||||
<MiniArticlePreview key={article.title} article={article} />
|
||||
))}
|
||||
</RemainingArticles>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { ArticleGrid };
|
||||
82
content/templates/react/components/article/preview/index.tsx
Normal file
82
content/templates/react/components/article/preview/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Title1 } from "@/typography";
|
||||
import { createTheme } from "@/theme/create";
|
||||
import { ThemeProvider } from "@/theme/provider";
|
||||
import { Article } from "types";
|
||||
|
||||
type Props = {
|
||||
article: Article;
|
||||
};
|
||||
|
||||
const Wrapper = styled.a`
|
||||
height: 500px;
|
||||
border-right: 2px solid rgba(0, 0, 0, 0.1);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
margin: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
max-height: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Title1)`
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
line-height: 40px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
font-size: 25px;
|
||||
padding: 0 5px;
|
||||
margin: 5px 0;
|
||||
`;
|
||||
|
||||
const MetaWrapper = styled.div`
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const AsideWrapper = styled.aside<{
|
||||
image?: string;
|
||||
}>`
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
||||
flex: 1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ArticlePreview: React.FC<Props> = ({ article }) => {
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
baseColor: article.color,
|
||||
}),
|
||||
[article.color]
|
||||
);
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Wrapper href={`/articles/${article.slug}`}>
|
||||
<AsideWrapper image={article.thumbUrl} />
|
||||
<MetaWrapper>
|
||||
{article.title.split(" ").map((word, index) => (
|
||||
<Title key={index}>{word}</Title>
|
||||
))}
|
||||
</MetaWrapper>
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticlePreview;
|
||||
79
content/templates/react/components/article/preview/jumbo.tsx
Normal file
79
content/templates/react/components/article/preview/jumbo.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Title1, Body1 } from "@/typography";
|
||||
import { Article } from "types";
|
||||
|
||||
type Props = {
|
||||
article: Article;
|
||||
};
|
||||
|
||||
const Wrapper = styled.a`
|
||||
height: 300px;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background: ${({ theme }) => theme.colors.background};
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
flex-direction: column;
|
||||
height: 500px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Title1)`
|
||||
line-height: 40px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
font-size: 25px;
|
||||
padding: 0 5px;
|
||||
margin: 5px 0;
|
||||
`;
|
||||
|
||||
const Summery = styled(Body1)`
|
||||
max-width: 300px;
|
||||
padding: 0 5px;
|
||||
margin: 5px 0;
|
||||
overflow: hidden;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 2.1rem;
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
max-height: 100px;
|
||||
}
|
||||
`;
|
||||
|
||||
const MetaWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
`;
|
||||
|
||||
const AsideWrapper = styled.aside<{
|
||||
image?: string;
|
||||
}>`
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
||||
flex: 1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const JumboArticlePreview: React.FC<Props> = ({ article }) => {
|
||||
return (
|
||||
<Wrapper href={`/articles/${article.slug}`}>
|
||||
<AsideWrapper image={article.coverUrl} />
|
||||
<MetaWrapper>
|
||||
<Title>{article.title}</Title>
|
||||
<Summery>{article.content}</Summery>
|
||||
</MetaWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { JumboArticlePreview };
|
||||
80
content/templates/react/components/article/preview/mini.tsx
Normal file
80
content/templates/react/components/article/preview/mini.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Title1 } from "@/typography";
|
||||
import { createTheme } from "@/theme/create";
|
||||
import { ThemeProvider } from "@/theme/provider";
|
||||
import { Article } from "types";
|
||||
|
||||
type Props = {
|
||||
article: Article;
|
||||
};
|
||||
|
||||
const Wrapper = styled.a`
|
||||
position: relative;
|
||||
margin: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
width: 220px;
|
||||
height: 200px;
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Title1)`
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
padding: 5px 5px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
margin: 5px 0;
|
||||
background: ${({ theme }) => theme.colors.background};
|
||||
`;
|
||||
|
||||
const MetaWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
max-width: 220px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
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 MiniArticlePreview: React.FC<Props> = ({ article }) => {
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
baseColor: article.color,
|
||||
}),
|
||||
[article.color]
|
||||
);
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Wrapper href={`/articles/${article.slug}`}>
|
||||
<AsideWrapper image={article.thumbUrl} />
|
||||
<MetaWrapper>
|
||||
{article.title.split(" ").map((word, index) => (
|
||||
<Title key={index}>{word}</Title>
|
||||
))}
|
||||
</MetaWrapper>
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { MiniArticlePreview };
|
||||
30
content/templates/react/components/html/index.tsx
Normal file
30
content/templates/react/components/html/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { FC, ReactNode } from "react"
|
||||
|
||||
type HtmlProps = {
|
||||
body: ReactNode;
|
||||
head: ReactNode;
|
||||
scripts: string[];
|
||||
};
|
||||
|
||||
const Html: FC<HtmlProps> = ({ body, head, scripts }) => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<title>My App</title>
|
||||
{head}
|
||||
{scripts.map((script, index) => (
|
||||
<script key={index} src={script} />
|
||||
))}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">{body}</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export { Html };
|
||||
65
content/templates/react/components/sheet/index.tsx
Normal file
65
content/templates/react/components/sheet/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
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: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const BackgroundWrapper = styled.div<{
|
||||
image?: string;
|
||||
}>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.2;
|
||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
background?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const Sheet: React.FC<Props> = ({ color, background, children }) => {
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
baseColor: color,
|
||||
}),
|
||||
[color]
|
||||
);
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Wrapper>
|
||||
<BackgroundWrapper image={background} />
|
||||
<Content>{children}</Content>
|
||||
</Wrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export { Sheet };
|
||||
160
content/templates/react/frontpage.tsx
Normal file
160
content/templates/react/frontpage.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
import { ArticleGrid } from "@/components/article/grid";
|
||||
import { Jumbo } from "@/typography";
|
||||
import { useMemo } from "react";
|
||||
import { Sheet } from "./components/sheet";
|
||||
import { ThemeProvider, createTheme } from "./theme";
|
||||
import chroma from "chroma-js";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Page } from "../../../types";
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* { box-sizing: border-box; }
|
||||
body, html { height: 100%; margin: 0; }
|
||||
body {
|
||||
font-size: 17px;
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
color: ${({ theme }) => theme.colors.foreground};
|
||||
}
|
||||
`;
|
||||
|
||||
const Hero = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const Download = styled.a`
|
||||
font-size: 30px;
|
||||
line-height: 40px;
|
||||
display: inline-block;
|
||||
background: ${({ theme }) => theme.colors.foreground};
|
||||
color: ${({ theme }) => theme.colors.primary};
|
||||
padding: 0 15px;
|
||||
text-transform: uppercase;
|
||||
margin: 10px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
@media only screen and (max-width: 700px) {
|
||||
margin: 5px;
|
||||
font-size: 3rem;
|
||||
line-height: 3.1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
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: 10px;
|
||||
font-family: "Black Ops One", sans-serif;
|
||||
@media only screen and (max-width: 700px) {
|
||||
margin: 5px;
|
||||
font-size: 3rem;
|
||||
line-height: 3.1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Arrow = styled.div`
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
:after {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${({ theme }) => theme.colors.primary};
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
content: "↓";
|
||||
font-size: 50px;
|
||||
@media only screen and (max-width: 700px) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ImageBg = styled.picture`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
z-index: -1;
|
||||
opacity: 0.5;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
baseColor: chroma.random().brighten(1).hex(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Helmet>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Helmet>
|
||||
<GlobalStyle />
|
||||
<Sheet color="#c85279">
|
||||
<ImageBg>
|
||||
<img src={profile.imageUrl} loading="lazy" />
|
||||
</ImageBg>
|
||||
<Arrow />
|
||||
<Hero>
|
||||
{"Hi, I'm Morten".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>
|
||||
<Hero>
|
||||
<Download href="/resume.pdf" download>
|
||||
Download resumé
|
||||
</Download>
|
||||
</Hero>
|
||||
</Sheet>
|
||||
<Sheet color="#ef23e2">
|
||||
<Hero>
|
||||
{"Table of Content".split(" ").map((char, index) => (
|
||||
<Title key={index}>{char}</Title>
|
||||
))}
|
||||
</Hero>
|
||||
<ArticleGrid articles={articles} />
|
||||
</Sheet>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrontPage;
|
||||
60
content/templates/react/theme/create.ts
Normal file
60
content/templates/react/theme/create.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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.01);
|
||||
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 };
|
||||
|
||||
6
content/templates/react/theme/global.d.ts
vendored
Normal file
6
content/templates/react/theme/global.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import {} from 'styled-components';
|
||||
import { Theme } from './theme';
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme extends Theme {}
|
||||
}
|
||||
|
||||
5
content/templates/react/theme/index.ts
Normal file
5
content/templates/react/theme/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createTheme } from './create';
|
||||
import { ThemeProvider } from './provider';
|
||||
|
||||
export * from './theme';
|
||||
export { createTheme, ThemeProvider };
|
||||
14
content/templates/react/theme/provider.tsx
Normal file
14
content/templates/react/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
content/templates/react/theme/theme.ts
Normal file
30
content/templates/react/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 };
|
||||
28
content/templates/react/tsconfig.json
Normal file
28
content/templates/react/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"types/*": ["../../../types/*"],
|
||||
"types": ["../../../types"]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"files": true
|
||||
},
|
||||
"include": ["./**/*", "../../../types/**/*"],
|
||||
}
|
||||
57
content/templates/react/typography/index.tsx
Normal file
57
content/templates/react/typography/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import styled from "styled-components";
|
||||
import { Theme, Typography } from "../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 };
|
||||
Reference in New Issue
Block a user