This commit is contained in:
Morten Olsen
2025-12-02 22:38:20 +01:00
parent 1693a2620c
commit 06564dff21
117 changed files with 2522 additions and 7180 deletions

226
src/pages/about.astro Normal file
View File

@@ -0,0 +1,226 @@
---
import { Picture } from "astro:assets";
import Html from "~/components/page/Html.astro";
import { data } from "~/data/data"
import { formatMonthYear, positionWithTeam } from "~/utils/utils.format";
const { Content, ...profile } = data.profile;
const experiences = await data.experiences.getAll();
const skills = await data.skills.getAll();
const jsonLd = await profile.getJsonLd();
---
<Html
title={profile.name}
description={profile.name}
jsonLd={jsonLd}
>
<main class="main">
<div class="content">
<Content />
</div>
<div class="profiles">
<h2>Profiles</h2>
<ul>
{Object.entries(profile.profiles).map(([id, profile]) => (
<li class="profile">
<a href={profile.url} target="_blank">{profile.network?.name || id}</a>
</li>
))}
</ul>
</div>
<div class="experiences">
<h2>Experiences</h2>
<div class="content">
{experiences.map((experience) => (
<div data-fadein class="experience">
{experience.data.logo && (
<div class="logo">
<Picture
alt='thumbnail image'
fetchpriority="auto"
src={experience.data.logo}
formats={['avif', 'webp', 'jpeg']}
width={50}
/>
</div>
)}
<div class="header">
<a class="companyName" href={`/experiences/${experience.id}`}><h3>{experience.data.company.name}</h3></a>
<a class="position" href={`/experiences/${experience.id}`}><h3>{positionWithTeam(experience.data.position.name, experience.data.position.team)}</h3></a>
</div>
<div class="time">
<div class="from">
{formatMonthYear(experience.data.startDate)}
</div>
<div class="to">
{formatMonthYear(experience.data.endDate)}
</div>
</div>
{experience.data.stack && (
<div class="stack">
<ul>
{experience.data.stack.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
</div>
<div class="skills">
<h2>Technologies</h2>
{skills.map((skill) => (
<div class="skill" data-fadein>
<a class="name" href={`/skills/${skill.id}`}><h3>{skill.data.name}</h3></a>
<div class="technologies">
<ul>
{skill.data.technologies.map((technology) => (
<li>{technology}</li>
))}
</ul>
</div>
</div>
))}
</div>
</main>
</Html>
<script src="../scripts/fadein.ts"></script>
<style>
.main {
max-width: var(--content-width);
margin: auto;
display: grid;
gap: calc(var(--gap) * 3);
grid-template-columns: 200px 1fr;
grid-template-rows: auto;
grid-template-areas:
"content content"
"profiles profiles"
"skills experiences";
h2 {
font-weight: var(--fw-md);
}
}
.content {
grid-area: content;
}
.profiles {
grid-area: profiles;
display: flex;
gap: var(--gap);
ul {
display: contents;
}
}
.skills {
grid-area: skills;
display: flex;
flex-direction: column;
gap: var(--gap);
.skill {
display: flex;
flex-direction: column;
gap: var(--gap);
.technologies ul {
display: flex;
flex-wrap: wrap;
gap: calc(var(--gap) / 2);
li {
border: solid var(--c-line) 1px;
padding: 3px 8px;
border-radius: var(--radius);
font-size: 12px;
}
}
}
}
.experiences {
grid-area: experiences;
.content {
display: flex;
flex-direction: column;
gap: calc(var(--gap) * 2);
}
.experience {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto;
column-gap: var(--gap);
row-gap: calc(var(--gap) / 2);
align-items: center;
grid-template-areas:
"logo header time"
"logo stack time";
.logo {
grid-area: logo;
img {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
object-position: center center;
}
}
.time {
grid-area: time;
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-items: center;
}
.companyName {
grid-area: companyName;
font-weight: var(--fw-md);
}
.position {
grid-area: position;
font-size: var(--fs-sm);
}
.summary{
grid-area: summary;
}
.stack {
grid-area: stack;
ul {
display: flex;
flex-wrap: wrap;
gap: calc(var(--gap) / 4);
li {
border: solid var(--c-line) 1px;
padding: 3px 8px;
border-radius: var(--radius);
font-size: 12px;
}
}
}
}
}
</style>

View File

@@ -1,20 +0,0 @@
---
import { type Article, data } from '@/data/data.js'
import ArticleView from '@/layouts/article/article.astro'
type Props = {
article: Article
}
export const getStaticPaths = async () => {
const articles = await data.articles.find()
return articles.map((article) => ({
params: { slug: article.data.slug },
props: { article }
}))
}
const { props } = Astro
const { article } = props
---
<ArticleView article={article} />

View File

@@ -1,36 +0,0 @@
---
import { type Article, data } from '@/data/data.js'
import Articles from '@/layouts/articles/articles.astro'
import { range } from '@/utils/data.js'
type Props = {
articles: Article[]
pageNumber: number
pageCount: number
pageSize: number
}
export async function getStaticPaths() {
const pageSize = 2
const allArticles = await data.articles.find()
const pageCount = Math.ceil(allArticles.length / pageSize)
const pages = range(0, pageCount).map((index) => {
const start = index * pageSize
const end = start + pageSize
return {
pageNumber: index + 1,
pageCount,
pageSize,
articles: allArticles.slice(start, end)
}
})
return pages.map((page) => ({
params: { page: String(page.pageNumber) },
props: page
}))
}
const { props } = Astro
---
<Articles {...props} />

View File

@@ -1,18 +0,0 @@
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { data } from '@/data/data.ts';
export async function GET(context: APIContext) {
const articles = await data.articles.find();
const profile = data.profile;
return rss({
title: profile.basics.name,
description: profile.basics.tagline,
site: context.site || 'http://localhost:3000',
items: articles.map((article) => ({
...article.data,
link: `/articles/${article.data.slug}/`,
})),
});
}

View File

@@ -0,0 +1,47 @@
---
import type { GetStaticPaths } from "astro";
import { Picture } from "astro:assets";
import { render } from "astro:content";
import Html from "~/components/page/Html.astro";
import { data } from "~/data/data";
import { formatMonthYear, positionWithTeam } from "~/utils/utils.format";
export const getStaticPaths = (async () => {
const experiences = await data.experiences.getAll();
return experiences.map((experience) => ({
params: { id: experience.id }
}));
}) satisfies GetStaticPaths;
const experience = await data.experiences.get(Astro.params.id)
const { Content } = await render(experience);
---
<Html
title={experience.data.slug}
description={experience.data.slug}
>
<main>
<h1>{experience.data.company.name}</h1>
<h2>{positionWithTeam(experience.data.position.name, experience.data.position.team)}</h2>
{experience.data.logo && (
<Picture
alt='thumbnail image'
fetchpriority="auto"
src={experience.data.logo}
formats={['avif', 'webp', 'jpeg']}
width={50}
/>
)}
{formatMonthYear(experience.data.startDate)}
{formatMonthYear(experience.data.endDate)}
<Content />
</main>
</Html>
<style>
main {
max-width: var(--content-width);
margin: auto;
}
</style>

View File

@@ -1,6 +1,89 @@
---
import {} from '@/data/data.js'
import Frontpage from '@/layouts/frontpage/frontpage.astro'
import { Picture } from "astro:assets";
import AbsoluteTime from "~/components/base/time/AbsoluteTime.astro";
import Html from "~/components/page/Html.astro";
import { data } from "~/data/data"
const { Content, ...profile } = data.profile;
const posts = await data.posts.getPublished();
const jsonLd = await profile.getJsonLd();
---
<Frontpage />
<Html
title={profile.name}
description={profile.name}
jsonLd={jsonLd}
>
<main class="main">
<div class="content">
<Content />
</div>
<h2>Posts</h2>
<div class="posts">
{posts.map((post) => (
<div class="post" data-fadein>
<a href={`/posts/${post.id}`}>
<Picture
alt='thumbnail image'
src={post.data.heroImage}
formats={['avif', 'webp', 'jpeg']}
fetchpriority="auto"
widths={[100, 200, 300]}
/>
</a>
<a class="title" href={`/posts/${post.id}`}><h3>{post.data.title}</h3></a>
<div class="subtitle"><AbsoluteTime datetime={post.data.pubDate} /></div>
</div>
))}
</div>
</main>
</Html>
<script src="../scripts/fadein.ts"></script>
<style>
.main {
max-width: var(--content-width);
margin: auto;
display: flex;
flex-direction: column;
gap: calc(var(--gap) * 3);
padding-bottom: 50px;
h2 {
font-weight: var(--fw-md);
}
}
.posts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: calc(var(--gap) * 2);
.post {
img {
width: 100%;
max-height: 250px;
object-fit: cover;
border-radius: var(--radius);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.3);
}
.title {
font-weight: var(--fw-md);
}
.subtitle {
font-size: var(--fs-sm);
}
&:hover {
transform: scale(1.05);
}
}
}
</style>

View File

@@ -1,21 +1,21 @@
import type { ManifestOptions } from 'vite-plugin-pwa'
import { icons } from '@/assets/images/icons.js'
import { data } from '@/data/data.js'
import { icons } from '~/assets/images/images.icons';
import { data } from '~/data/data';
export async function GET() {
const [maskableIcon] = icons.pngs.filter(
(icon) => icon.size === '512x512' && icon.src.includes('png')
)
const nonMaskableIcons = icons.pngs.filter((icon) => icon !== maskableIcon)
const basics = data.profile.basics
const { profile } = data;
const manifest: Partial<ManifestOptions> = {
name: basics.name,
short_name: basics.name,
description: basics.tagline,
name: profile.name,
short_name: profile.name,
// description: basics.tagline,
theme_color: '#30E130',
background_color: data.site.theme,
// background_color: data.site.theme,
start_url: '/',
display: 'minimal-ui',
icons: [
@@ -26,13 +26,13 @@ export async function GET() {
})),
...(maskableIcon
? [
{
src: maskableIcon.src,
sizes: maskableIcon.size,
type: 'image/png',
purpose: 'maskable'
}
]
{
src: maskableIcon.src,
sizes: maskableIcon.size,
type: 'image/png',
purpose: 'maskable'
}
]
: [])
]
}

View File

@@ -0,0 +1,255 @@
---
import Html from '~/components/page/Html.astro';
import type { GetStaticPaths } from "astro";
import { render } from "astro:content";
import { data } from '~/data/data'
import { Picture } from 'astro:assets';
export const getStaticPaths = (async () => {
const posts = await data.posts.getPublished();
return posts.map((post) => ({
params: { id: post.id }
}));
}) satisfies GetStaticPaths;
const post = await data.posts.get(Astro.params.id);
const { Content } = await render(post);
---
<Html
title={post.data.title}
description={post.data.description}
jsonLd={post.jsonLd}
image={`/posts/${post.id}/share.png`}
>
<article>
<header>
<h1>
{post.data.title.split(' ').map((word) => <span>{word}</span>)}
</h1>
<a href='/'><h2>By {data.profile.name}</h2></a>
</header>
<Picture
loading='eager'
class='img'
src={post.data.heroImage}
widths={[320, 640, 1024, 1400]}
formats={['avif', 'webp', 'png']}
alt='Cover image'
/>
<div class='content'>
<Content />
</div>
</article>
</Html>
<style>
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-family-heading);
font-weight: 700;
line-height: 1.2;
margin: 0;
}
article {
--left-padding: 100px;
font-family: var(--font-family);
font-size: var(--font-size);
letter-spacing: var(--letter-spacing);
color: var(--color);
background-color: var(--background-color);
display: grid;
letter-spacing: 0.05rem;
font-size: 1rem;
line-height: 1.3rem;
grid-template-columns: 1fr calc(60ch + var(--left-padding)) 2fr;
grid-template-rows: auto;
grid-template-areas:
'. title cover'
'. content cover';
}
article :global(picture) {
grid-area: cover;
position: relative;
}
.img {
max-width: 100%;
height: 100vh;
top: 0;
position: sticky;
object-fit: cover;
object-position: center;
right: 0;
clip-path: polygon(40% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%);
}
header {
grid-area: title;
min-height: 80vh;
display: flex;
justify-content: center;
flex-direction: column;
.subtitle {
font-size: 1rem;
line-height: 2rem;
padding-top: 1rem;
padding-bottom: 1rem;
text-transform: uppercase;
}
}
h2 {
font-size: 1.5rem;
font-weight: 300;
color: #fff;
text-transform: uppercase;
margin-top: var(--space-md);
color: #000;
}
h3 {
font-size: 1.1rem;
}
h1 {
display: flex;
flex-wrap: wrap;
font-size: 4rem;
line-height: 1;
color: #fff;
text-transform: uppercase;
font-weight: 400;
gap: 1rem;
span {
display: inline-block;
background: red;
padding: 0.5rem 1rem;
}
}
.content {
grid-area: content;
padding: var(--space-xl);
padding-left: var(--left-padding);
img {
max-width: 100%;
height: auto;
margin-bottom: var(--space-lg);
}
p {
text-align: justify;
margin-bottom: var(--space-md);
line-height: 1.5rem;
}
p:first-of-type {
&:first-letter {
font-size: 5rem;
border: 5px solid #000;
float: left;
padding: 0 var(--space-md);
margin-right: 1rem;
line-height: 1;
}
}
h2 {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: var(--space-lg);
padding-top: var(--space-lg);
text-transform: uppercase;
&:first-letter {
background: #000;
color: #fff;
padding: 0 5px;
letter-spacing: -0.2rem;
}
}
h3 {
font-size: 1rem;
text-transform: uppercase;
font-weight: 900;
margin-bottom: 1rem;
&:first-letter {
font-size: 1.2rem;
}
}
strong {
background: #000;
padding: 0 5px;
color: #fff;
font-weight: 600;
}
em {
font-style: italic;
}
li {
list-style-type: circle;
margin-left: 2rem;
padding-bottom: var(--space-md);
}
code {
font-family: monospace;
}
}
@media (max-width: 1024px) {
article {
--left-padding: 0;
grid-template-columns: 1fr;
grid-template-areas:
'title'
'cover'
'content';
}
article picture {
position: absolute;
z-index: -1;
height: 80vh;
}
.img {
clip-path: none;
height: 80vh;
opacity: 0.5;
}
header {
padding: var(--space-xl);
height: 80vh;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.2rem;
}
.content {
padding: var(--space-lg);
}
}
</style>

View File

@@ -0,0 +1,35 @@
import type { APIContext, GetStaticPaths } from 'astro';
import { data } from '~/data/data';
import { createCanvas } from 'canvas';
const getStaticPaths = (async () => {
const posts = await data.posts.getPublished();
return posts.map((post) => ({
params: { id: post.id }
}));
}) satisfies GetStaticPaths;
const GET = async (context: APIContext<Record<string, string>, { id: string }>) => {
const { id } = context.params;
const post = await data.posts.get(id);
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')
ctx.fillText(post.data.title, 10, 10)
const buffer = await new Promise<Buffer>((resolve, reject) => {
canvas.toBuffer((err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
})
})
return new Response(buffer, {
headers: {
'Content-Type': 'image/png',
}
})
}
export { GET, getStaticPaths }

View File

@@ -0,0 +1,17 @@
---
import { getCollection } from 'astro:content';
import Html from '~/components/page/Html.astro';
const posts = await getCollection('posts');
---
<Html
title="Morten Olsen's Posts"
description='Foo'
>
<h1>Posts</h1>
{posts.map((post) => (
<a href={`/posts/${post.id}`}>{post.data.title}</a>
))}
</Html>

View File

@@ -0,0 +1,19 @@
import rss from '@astrojs/rss';
import type { APIContext } from 'astro';
import { data } from '~/data/data';
const GET = async (context: APIContext) => {
const posts = await data.posts.getPublished();
return rss({
title: data.profile.name,
description: 'Bar',
site: context.site || 'http://localhost:3000',
items: posts.map((post) => ({
...post.data,
link: `/posts/${post.id}`,
})),
});
}
export { GET }

View File

@@ -1,6 +1,6 @@
import { data } from '@/data/data.ts'
import { data } from "~/data/data"
export async function GET() {
const resume = await data.getJsonResume()
const resume = await data.profile.getResumeJson();
return new Response(JSON.stringify(resume, null, 2))
}

View File

@@ -0,0 +1,18 @@
---
import type { GetStaticPaths } from "astro";
import { render } from "astro:content";
import { data } from "~/data/data";
export const getStaticPaths = (async () => {
const skills = await data.skills.getAll();
return skills.map((skill) => ({
params: { id: skill.id }
}));
}) satisfies GetStaticPaths;
const skill= await data.skills.get(Astro.params.id)
const { Content } = await render(skill);
---
<h1>Hello {skill.data.name}</h1>
<Content />

View File

@@ -1,5 +0,0 @@
---
import Work from '@/layouts/work-history/work-history.astro'
---
<Work />