Updated portfolio
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user