diff --git a/package-lock.json b/package-lock.json index a5d0ece..a19f760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react": "^18.3.1", "react-content-loader": "^7.0.2", "react-dom": "^18.3.1", + "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.6.1", @@ -3393,6 +3394,18 @@ "react": "^18.3.1" } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3963,6 +3976,15 @@ } } }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index dc83c0a..0d08ab5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "react": "^18.3.1", "react-content-loader": "^7.0.2", "react-dom": "^18.3.1", + "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.6.1", diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/api/blog.ts b/src/app/api/blog.ts index 40da058..1c0a030 100644 --- a/src/app/api/blog.ts +++ b/src/app/api/blog.ts @@ -9,7 +9,10 @@ interface Blog { description: string, }; -export async function queryDatabase(notion: Client, databaseId: string): Promise<[Array, any]> { +type QueryResponse = { cursor: string | null, hasMore: boolean, items: Blog[] }; +type Error = { msg: string }; + +export async function getBlogs(notion: Client, databaseId: string, cursor: string | undefined, pageSize: number): Promise<[number, QueryResponse | Error]> { try { const response = await notion.databases.query({ database_id: databaseId, @@ -18,22 +21,16 @@ export async function queryDatabase(notion: Client, databaseId: string): Promise status: { equals: "Posted" } - } + }, + start_cursor: cursor, + page_size: pageSize, }); - return [response.results, null]; - } catch (error: any) { - return [[], error]; + return [200, {cursor: response.next_cursor, hasMore: response.has_more, items: response.results.map(convertBlog)}]; + } catch (err: any) { + return [400, {msg: String(err)}]; } } -export default async function getBlogs(notion: Client, databaseId: string): Promise<[number, Blog[] | {msg: string}]> { - const [pages, err] = await queryDatabase(notion, databaseId); - if (err === null) { - return [200, pages.map(convertBlog)]; - } - return [500, {msg: String(err)}]; -} - function convertBlog(page: any): Blog { return { id: page.id, @@ -48,7 +45,7 @@ interface FullBlog extends Blog { markdown: string, }; -export async function getBlog(notion: Client, n2m: NotionToMarkdown, pageId: string): Promise<[number, FullBlog | {msg: string}]> { +export async function getBlog(notion: Client, n2m: NotionToMarkdown, pageId: string): Promise<[number, FullBlog | Error]> { try { const page: any = await notion.pages.retrieve({ page_id: pageId }) if (page.properties.Status.status.name !== 'Posted') @@ -62,7 +59,7 @@ export async function getBlog(notion: Client, n2m: NotionToMarkdown, pageId: str description: blog.description, tags: blog.tags, date: blog.date, - markdown: n2m.toMarkdownString(mdBlocks).parent, + markdown: n2m.toMarkdownString(mdBlocks).parent || '', }]; } catch (err: any) { return [500, {msg: String(err)}]; diff --git a/src/app/api/route.ts b/src/app/api/route.ts index 6332ff6..50d8daa 100644 --- a/src/app/api/route.ts +++ b/src/app/api/route.ts @@ -1,7 +1,12 @@ -import getBlogs from './blog'; +import { getBlogs } from './blog'; import { notion, databaseId } from './notion'; -export async function GET() { - let [status, data] = await getBlogs(notion, databaseId); - return Response.json(data, { status }); -} \ No newline at end of file +export async function GET(request: Request) { + const url = new URL(request.url); + var cursor = url.searchParams.get('cursor') || undefined; + if (cursor === '') + cursor = undefined; + const size = parseInt(url.searchParams.get('size') || '5'); + let [status, response] = await getBlogs(notion, databaseId, cursor, size); + return Response.json(response, { status }); +} diff --git a/src/app/Blog.css b/src/app/blog/Blog.css similarity index 96% rename from src/app/Blog.css rename to src/app/blog/Blog.css index a863f49..35270fc 100644 --- a/src/app/Blog.css +++ b/src/app/blog/Blog.css @@ -36,7 +36,7 @@ h1 { } .name { - font-family: 'Roboto Mono'; + font-family: sans-serif; color: #f5304c; text-align: center; font-weight: 800; @@ -59,7 +59,7 @@ a { } .container { - margin: 100px auto; + margin: 10px auto; width: 600px; } diff --git a/src/app/blog/Loading.tsx b/src/app/blog/Loading.tsx index aa16a2d..8b547da 100644 --- a/src/app/blog/Loading.tsx +++ b/src/app/blog/Loading.tsx @@ -3,56 +3,30 @@ import { useEffect, useState } from "react"; import ContentLoader from "react-content-loader" -export default function LoadingBlog(props: any) { - const [numComponents, setNumComponents] = useState(0); +export function LoadingBlog(props: any) { const [isClient, setIsClient] = useState(false); useEffect(() => { - // Only set to client after mount setIsClient(true); }, []); - useEffect(() => { - const updateComponentCount = () => { - const windowHeight = window.innerHeight; // Get window height - const componentHeight = 100; // Set the height of your component - const count = Math.floor(windowHeight / componentHeight); // Calculate how many components fit - setNumComponents(count); - }; - - // Initial calculation - updateComponentCount(); - - // Add resize event listener - window.addEventListener('resize', updateComponentCount); - - // Cleanup listener on unmount - return () => { - window.removeEventListener('resize', updateComponentCount); - }; - }, []); // Empty dependency array ensures the effect runs once after the initial render - - // Only render on the client if (!isClient) { return null; } return (
- {Array.from({ length: numComponents }, (_, index) => ( - - - - - - ))} + + + + +
); } @@ -67,29 +41,24 @@ export function LoadingBlogRead(props: any) { useEffect(() => { const updateRectCount = () => { - // Calculate the number of rectangles based on window height const windowHeight = window.innerHeight; - const rectHeight = 46; // Height of each rectangle - const count = Math.floor(windowHeight / rectHeight); // Number of rectangles that fit + const rectHeight = 46; + const count = Math.floor(windowHeight / rectHeight); setNumRects(count); }; - // Update on mount and window resize updateRectCount(); window.addEventListener('resize', updateRectCount); - // Cleanup event listener on unmount return () => { window.removeEventListener('resize', updateRectCount); }; }, []); - // Only render on the client if (!isClient) { return null; } - // Dynamically set the viewBox height based on the number of rectangles const viewBoxHeight = window.innerHeight / 3; return ( @@ -106,10 +75,10 @@ export function LoadingBlogRead(props: any) { {Array.from({ length: numRects }).map((_, i) => ( diff --git a/src/app/blog/[id]/page.tsx b/src/app/blog/[id]/page.tsx index f198f30..d8183d0 100644 --- a/src/app/blog/[id]/page.tsx +++ b/src/app/blog/[id]/page.tsx @@ -1,65 +1,45 @@ "use client"; import React, { useEffect, useState } from 'react'; -import '../../Blog.css'; +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({ title: '', date: '', markdown: '' }); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(true); + const [blog, setBlog] = useState(null); - const date = new Date(blog.date).toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' }); - - useEffect(() => { - const fetchBlog = async () => { - try { - console.log('obtaining blog') - const response = await fetch(`/api/${(await params).id}`); - if (response.ok) { - const blog = await response.json(); - document.title = blog.title + " - Leo's blog"; - setBlog(blog); - } else { - const errorData = await response.text(); - setError(errorData); - } - } catch (err: any) { - setError(err); - } finally { - setLoading(false); - } - }; - fetchBlog(); - }, [params]); - - if (loading) { - return
-

Leo's blog

-
- -
-
; - } - - if (error) { + useState(() => { + params.then(({id}) => { + fetch(`/api/${id}`).then((res) => { + res.json().then((data) => { + setBlog(data); + }).catch((err) => { + console.error(err); + }); + }); + }); + }); + + if (blog !== null) { return (

Leo's blog

-
Error: {error}
; +
+

{blog.title}

+

{new Date(blog.date).toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' })}

+
{blog.markdown.replaceAll('\n-\n', '\n
\n')}
+
- ) + ); } - - return ( -
-

Leo's blog

-
-

{blog.title}

-

{date}

-
{blog.markdown.replaceAll('\n-\n', '\n
\n')}
-
-
- ); + else + return ( +
+

Leo's blog

+
+ +
+
+ ); } diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 2a9ddf9..1d37016 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,143 +1,129 @@ "use client"; import React, { useEffect, useState } from 'react'; -import '../Blog.css'; -import LoadingBlog from './Loading'; +import './Blog.css'; +import { LoadingBlog } from './Loading'; +import InfiniteScroll from "react-infinite-scroll-component"; -type Blog = { title: string, description: string, id: string, date: string, tags: { name: string, color: string }[] }; +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" - } + 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 [data, setData] = useState([]); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(true); + const [items, setItems] = useState([]); + const [hasMore, setHasMore] = useState(true); + const [cursor, setCursor] = useState(''); - useEffect(() => { - const fetchData = async () => { - setTimeout(async () => { - try { - const response = await fetch('/api/'); - if (response.ok) { - const result = await response.json(); - setData(result); - setLoading(false); - } else { - const errorData = await response.text(); - setError(errorData); - } - } catch (err: any) { - setError(err.toString()); - } - }, 500); - }; - fetchData(); - }, []); - - - if (error) { - if (error) { - return ( -
-

Leo's blog

-
Error: {error}
; -
- ) + 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); + }); } - } if (loading) { - return ( -
-

Leo's blog

- -
- ); + }; + + const refreshData = () => { + setItems([]); + setHasMore(true); + setCursor(''); } - document.title = "Leo's blog"; + useEffect(() => { + document.title = "Leo's blog"; + fetchData(); + }, []); return (

Leo's blog

); diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 511d313..55970e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,8 +2,8 @@ import type { Metadata } from "next"; import "./globals.css"; export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Leo's portfolio", + description: "Leo's portfolio" }; export default function RootLayout({ @@ -14,7 +14,11 @@ export default function RootLayout({ return ( Leo's portfolio - + + + + + {children}