Updated portfolio

This commit is contained in:
2025-01-03 16:39:52 +01:00
parent 49288242b2
commit b56c19207f
23 changed files with 2002 additions and 4184 deletions
-138
View File
@@ -1,138 +0,0 @@
@import url(https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700;800;900&display=swap);
@import url(https://fonts.googleapis.com/css?family=Roboto+Mono);
@import url(https://fonts.googleapis.com/css?family=Roboto);
p {
font-family: 'Roboto Mono';
color: white;
text-align: left;
}
body {
background-color: #101010;
margin: 0;
padding: 0;
}
h1 {
text-align: left;
}
.blog_title {
font-family: 'Montserrat';
color: white;
text-align: left;
font-weight: 800;
font-size: 36px;
}
.title {
font-family: 'Montserrat';
color: #f5304c;
text-align: left;
font-weight: 800;
font-size: 36px;
margin-top: -1%;
}
.name {
font-family: sans-serif;
color: #f5304c;
text-align: center;
font-weight: 800;
}
.date {
font-family: 'Montserrat';
font-size: 12px;
}
.description {
font-family: 'Montserrat';
font-size: 15px;
}
a {
color: #f5304c;
font-family: 'Roboto Mono';
text-decoration: none;
}
.container {
margin: 10px auto;
width: 600px;
}
@media (max-width: 600px) {
.container {
width: 100%;
}
}
.blog {
background-color: #151515;
border-radius: 10px;
border: 10px solid #151515;
margin: 20px;
transition: transform 0.4s ease;
overflow-wrap: break-word;
}
.blog.blog:hover {
transform: scale(1.03);
}
.blog_container {
margin: 60px auto;
width: 730px;
text-align: left;
overflow-wrap: break-word;
user-select: text;
overflow: hidden;
}
@media (max-width: 730px) {
.blog_container {
width: 100%;
}
}
/*
MARKDOWN
*/
.markdown {
font-family: 'Roboto Mono';
margin-top: 30px;
text-align: left;
}
.markdown img {
max-width: 100%;
}
ol li::marker {
color: #f5304c;
}
ul li::marker {
color: #f5304c;
}
.markdown code {
color: #f5304c;
}
.markdown h1, h2 {
color: #f5304c;
}
.markdown img {
padding: inherit;
margin-left: auto;
margin-right: auto;
display: block;
}
-88
View File
@@ -1,88 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import ContentLoader from "react-content-loader"
export function LoadingBlog(props: any) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return null;
}
return (
<div className="container">
<ContentLoader
speed={2}
viewBox="0 0 290 50"
backgroundColor="#3b3b3b"
foregroundColor="#f5304c"
{...props}
>
<rect x="0" y="0" rx="7" ry="7" width="290" height="18" />
<rect x="0" y="25" rx="3" ry="3" width="78" height="7" />
<rect x="0" y="37" rx="3" ry="3" width="200" height="7" />
</ContentLoader>
</div>
);
}
export function LoadingBlogRead(props: any) {
const [isClient, setIsClient] = useState(false);
const [numRects, setNumRects] = useState(0);
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
const updateRectCount = () => {
const windowHeight = window.innerHeight;
const rectHeight = 46;
const count = Math.floor(windowHeight / rectHeight);
setNumRects(count);
};
updateRectCount();
window.addEventListener('resize', updateRectCount);
return () => {
window.removeEventListener('resize', updateRectCount);
};
}, []);
if (!isClient) {
return null;
}
const viewBoxHeight = window.innerHeight / 3;
return (
<ContentLoader
speed={2}
viewBox={`0 0 340 ${viewBoxHeight}`}
backgroundColor="#3b3b3b"
foregroundColor="#f5304c"
style={{ marginTop: "20px" }}
{...props}
>
<rect x="0" y="0" rx="7" ry="7" width="290" height="18" />
<rect x="0" y="25" rx="3" ry="3" width="78" height="7" />
{Array.from({ length: numRects }).map((_, i) => (
<rect
x="0"
y={45 + i * 14}
rx="3"
ry="3"
width={Math.random() * (300 - 80) + 80}
height="9"
key={i}
/>
))}
</ContentLoader>
);
}
-60
View File
@@ -1,60 +0,0 @@
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import remarkMath from 'remark-math';
import rehypeRaw from 'rehype-raw';
import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { gruvboxDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css';
type MarkdownRendererProps = {
children: string;
};
export default function MarkdownRenderer({ children: markdown }: MarkdownRendererProps) {
return (
<Markdown
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
rehypePlugins={[rehypeRaw, rehypeKatex]}
components={{
code({ node, inline, className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter style={gruvboxDark} PreTag="div" language={match[1]} {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
ul({ children, ...props }) {
return (
<ul style={{ listStyleType: 'disc', listStylePosition: 'inside' }} {...props}>
{children}
</ul>
);
},
ol({ children, ...props }) {
return (
<ol style={{ listStyleType: 'decimal', listStylePosition: 'inside' }} {...props}>
{children}
</ol>
);
},
li({ children, ...props }) {
return (
<li style={{ marginBottom: '4px' }} {...props}>
{children}
</li>
);
},
}}
>
{markdown}
</Markdown>
);
}
-80
View File
@@ -1,80 +0,0 @@
"use client";
import React, { useEffect, useState } from 'react';
import '../Blog.css';
import MarkdownRenderer from './MarkdownRenderer';
import { LoadingBlogRead } from '../Loading';
import { BlogT2 } from '../page';
export default function Blog({ params }: { params: Promise<{ id: string }> }) {
const [blog, setBlog] = useState<BlogT2 | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
params
.then(({ id }) => {
return fetch(`/api/${id}`);
})
.then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch blog');
}
return res.json();
})
.then((data) => {
if (isMounted) setBlog(data);
})
.catch((err) => {
if (isMounted) setError(err.message);
console.error(err);
});
return () => {
isMounted = false;
};
}, [params]);
if (error) {
return (
<main>
<a href="/blog"><h1 className="name">Leo's blog</h1></a>
<div className="blog_container">
<p className="error">Error loading blog: {error}</p>
</div>
</main>
);
}
if (blog !== null) {
return (
<main>
<a href="/blog"><h1 className="name">Leo's blog</h1></a>
<div className="blog_container">
<h2 className="blog_title">{blog.title}</h2>
<p className="date">
{new Date(blog.date).toLocaleString('default', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</p>
<div className="markdown">
<MarkdownRenderer>
{blog.markdown.replaceAll('\n-\n', '\n<br/>\n')}
</MarkdownRenderer>
</div>
</div>
</main>
);
}
return (
<main>
<a href="/blog"><h1 className="name">Leo's blog</h1></a>
<div className="blog_container">
<LoadingBlogRead />
</div>
</main>
);
}
-130
View File
@@ -1,130 +0,0 @@
"use client";
import React, { useEffect, useState } from 'react';
import './Blog.css';
import { LoadingBlog } from './Loading';
import InfiniteScroll from "react-infinite-scroll-component";
export type BlogT = {
title: string,
description: string,
id: string,
date: string,
tags: { name: string, color: string }[]
};
export type BlogT2 = {
title: string,
description: string,
id: string,
date: string,
tags: { name: string, color: string }[],
markdown: string,
};
const notionColors: { [index: string]: {
background: string,
color: string,
} } = {
default: { background: "#191919", color: "#D4D4D4" },
gray: { background: "#252525", color: "#9B9B9B" },
brown: { background: "#2E2724", color: "#A27763" },
orange: { background: "#36291F", color: "#CB7B37" },
yellow: { background: "#372E20", color: "#C19138" },
green: { background: "#242B26", color: "#4F9768" },
blue: { background: "#1F282D", color: "#447ACB" },
purple: { background: "#2A2430", color: "#865DBB" },
pink: { background: "#2E2328", color: "#BA4A78" },
red: { background: "#332523", color: "#BE524B" }
};
export default function Blog() {
const [items, setItems] = useState<BlogT[]>([]);
const [hasMore, setHasMore] = useState(true);
const [cursor, setCursor] = useState('');
const fetchData = () => {
if (hasMore) {
fetch(`/api/?cursor=${cursor}&size=10`).then((res) => {
res.json().then((data: { items: BlogT[], hasMore: boolean, cursor: string }) => {
setItems((prev) => [...prev, ...data.items]);
setHasMore(data.hasMore);
setCursor(data.cursor);
}).catch((err) => {
console.error(err);
});
}).catch((err) => {
console.error(err);
});
}
};
const refreshData = () => {
setItems([]);
setHasMore(true);
setCursor('');
}
useEffect(() => {
document.title = "Leo's blog";
fetchData();
}, []);
return (
<main>
<a href="/blog"><h1 className='name'>Leo's blog</h1></a>
<div className='container'>
<InfiniteScroll
pullDownToRefresh
pullDownToRefreshThreshold={50}
dataLength={items.length}
next={fetchData}
hasMore={hasMore}
loader={<LoadingBlog/>}
refreshFunction={refreshData}
endMessage={
<p style={{ textAlign: 'center' }}>
<a>Yay! You have seen it all</a>
</p>
}
>
{items.map((blog, i) => (
<a key={i} href={`/blog/${blog.id}`} style={{ textDecoration: 'none' }}>
<div className='blog'>
<h2 className='title'>{blog.title}</h2>
<p className='date'>{new Date(blog.date).toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' })}</p>
<p className='description'>{blog.description}</p>
{blog.tags.map((tag, i) => (
notionColors[tag.color] !== undefined ?
<span key={i} style={{
background: notionColors[tag.color].background,
color: notionColors[tag.color].color,
fontSize: '12px',
border: '5px solid ' + notionColors[tag.color].background,
fontFamily: 'sans-serif',
borderRadius: '5px',
display: 'inline-block',
marginTop: '8px',
marginLeft: i === 0 ? '0px' : '8px',
}}>{tag.name}</span>
:
<span key={i} style={{
background: tag.color,
color: tag.color,
fontSize: '12px',
border: '5px solid ' + tag.color,
fontFamily: 'sans-serif',
borderRadius: '5px',
display: 'inline-block',
marginTop: '8px',
marginLeft: i === 0 ? '0px' : '8px',
}}>{tag.name}</span>
))}
</div>
</a>
))}
</InfiniteScroll>
</div>
</main>
);
}