Blog
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { getBlog, getBlogs } from "@/blogApi";
|
||||
import { notion, databaseId, n2m } from "@/notion";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
if (id !== null) {
|
||||
let [status, data] = await getBlog(notion, n2m, id);
|
||||
return Response.json(data, { status });
|
||||
} else {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function GET(req: Request) {
|
||||
return new Response(JSON.stringify({
|
||||
version: "1.0.2",
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getBlogs, searchBlog } from "@/blogApi";
|
||||
import { notion, databaseId } from "@/notion";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
let [status, response] = await searchBlog(
|
||||
notion,
|
||||
url.searchParams.get("query") || ""
|
||||
);
|
||||
return Response.json(response, { status });
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import MarkdownRenderer from "@/components/MarkdownRenderer";
|
||||
import { useEffect, useState } from "react";
|
||||
import "./style.css";
|
||||
|
||||
type Blog = {
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
tags: { name: string; color: string }[];
|
||||
markdown: string;
|
||||
date: string;
|
||||
};
|
||||
|
||||
export default function Blog({ params }: { params: Promise<{ id: string }> }) {
|
||||
const [blog, setBlog] = useState<null | Blog>(null);
|
||||
|
||||
useEffect(() => {
|
||||
params.then(({ id }) =>
|
||||
fetch(`/api/blog/?id=${id}`)
|
||||
.then((res) => {
|
||||
res
|
||||
.json()
|
||||
.then((response) => {
|
||||
if (res.status === 200) {
|
||||
setBlog(response);
|
||||
} else {
|
||||
console.error(`'/api/blog/${id}' Error:`, response.msg);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`'/api/blog/${id}' Unable to obtain json:`, e);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`'/api/blog/${id}' Error:`, e);
|
||||
})
|
||||
);
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="content-center w-full lg:mx-auto pt-16 lg:pt-0 relative z-20 pb-16 md:pb-0 animate-fadeIn">
|
||||
<div className="w-full max-w-screen-lg mx-auto">
|
||||
<div className="px-6 md:px-10 lg:px-15 xl:px-2">
|
||||
<div className="pt-8 pb-6 px-8 bg-transparent-ish rounded-xl h-screen">
|
||||
{blog ? (
|
||||
<div>
|
||||
<h1 className="text-red-400">{blog.title}</h1>
|
||||
<p className="text-gray-400">
|
||||
{new Date(blog.date).toDateString()}
|
||||
</p>
|
||||
<div className="mt-4 blog-md">
|
||||
<MarkdownRenderer>
|
||||
{blog.markdown
|
||||
.replaceAll(/\n!\n/g, "\n<br/>\n")
|
||||
.replaceAll(/\n-/g, "\n\n-")}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.blog-md h1 {
|
||||
color: #fa5770;
|
||||
}
|
||||
|
||||
.blog-md a {
|
||||
color: #fa5770;
|
||||
}
|
||||
|
||||
.blog-md code {
|
||||
color: #fa5770;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Blog = {
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
tags: { name: string; color: string }[];
|
||||
};
|
||||
|
||||
export default function Blog() {
|
||||
const [blogs, setBlogs] = useState<Blog[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const fetchBlogs = (query: string) => {
|
||||
(query === ""
|
||||
? fetch("/api/blog/")
|
||||
: fetch(`/api/search/?query=${encodeURIComponent(query)}`)
|
||||
)
|
||||
.then((res) => {
|
||||
res
|
||||
.json()
|
||||
.then((response) => {
|
||||
if (response && response.items) {
|
||||
setBlogs(response.items);
|
||||
} else {
|
||||
console.error("`/api/blog/` Response is invalid:", response);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("`/api/blog/` Unable to obtain json:", e);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("`/api/blog/` Error:", e);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
fetchBlogs(searchQuery);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="content-center w-full lg:mx-auto pt-16 lg:pt-0 relative z-20 pb-16 md:pb-0 animate-fadeIn">
|
||||
<div className="w-full max-w-screen-lg mx-auto">
|
||||
<div className="px-6 md:px-10 lg:px-15 xl:px-2">
|
||||
<div className="pt-8 pb-6 px-8 bg-transparent-ish rounded-xl h-screen">
|
||||
<h1 className="text-red-400 w-max mx-auto">Leo's blog</h1>
|
||||
<input
|
||||
type="search"
|
||||
name="Blog"
|
||||
id="blog-search"
|
||||
className="p-3 bg-transparent border border-red rounded-xl w-72 mx-auto mt-5"
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder="Search"
|
||||
/>
|
||||
<div className="mt-10 grid gap-6 w-full lg:grid-cols-2">
|
||||
{blogs.map((blog, i) => (
|
||||
<div
|
||||
className="p-3 px-5 h-52 border border-red rounded-xl backdrop-blur-xl"
|
||||
key={i}
|
||||
>
|
||||
<h1 className="hover:text-red text-ellipsis overflow-hidden">
|
||||
<Link href={"/blog/" + blog.id}>{blog.title}</Link>
|
||||
</h1>
|
||||
<p className="pt-1 max-h-[72px] overflow-hidden">
|
||||
{blog.description}
|
||||
</p>
|
||||
<div className="flex gap-5 pt-3 flex-wrap overflow-hidden h-10">
|
||||
{blog.tags.map((tag, i) => (
|
||||
<span
|
||||
className="inline-flex items-center rounded-md border px-2.5 py-0.5 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground text-xs bg-red-600/10 border-red-600"
|
||||
key={i}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-14
@@ -1,17 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
import Background from "@/components/Background";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Leo's Portfolio",
|
||||
@@ -25,9 +15,8 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<body className={`antialiased`}>
|
||||
<Background />
|
||||
<Navbar />
|
||||
{children}
|
||||
</body>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Background from "@/components/Background";
|
||||
import PinnedRepos from "@/components/PinnedRepos";
|
||||
import Skill from "@/components/Skill";
|
||||
import {
|
||||
@@ -16,7 +15,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
export default function App() {
|
||||
return (
|
||||
<div>
|
||||
<Background />
|
||||
<div className="content-center w-full lg:mx-auto pt-16 lg:pt-0 relative z-20 pb-16 md:pb-0 animate-fadeIn">
|
||||
<div className="w-full max-w-screen-lg mx-auto">
|
||||
<div className="px-8 md:px-10 lg:px-15 xl:px-24">
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import Background from "@/components/Background";
|
||||
import {
|
||||
faCheck,
|
||||
faCode,
|
||||
faRocket,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCode, faRocket } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<div>
|
||||
<Background />
|
||||
{/* <Background /> */}
|
||||
<div className="content-center w-full lg:mx-auto pt-16 lg:pt-0 relative z-20 pb-16 md:pb-0 animate-fadeIn">
|
||||
<div className="w-full max-w-screen-lg mx-auto px-4 md:px-10">
|
||||
<h1 className="pt-10 w-max mx-auto text-red-400">Choose a plan</h1>
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { Client } from "@notionhq/client";
|
||||
import { SearchResponse } from "@notionhq/client/build/src/api-endpoints";
|
||||
import { NotionToMarkdown } from "notion-to-md";
|
||||
|
||||
interface Blog {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: { name: string; color: string }[];
|
||||
date: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
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,
|
||||
filter: {
|
||||
property: "Status",
|
||||
status: {
|
||||
equals: "Posted",
|
||||
},
|
||||
},
|
||||
start_cursor: cursor,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return [
|
||||
200,
|
||||
{
|
||||
cursor: response.next_cursor,
|
||||
hasMore: response.has_more,
|
||||
items: response.results.map(convertBlog),
|
||||
},
|
||||
];
|
||||
} catch (err: any) {
|
||||
return [400, { msg: String(err) }];
|
||||
}
|
||||
}
|
||||
|
||||
function convertBlog(page: any): Blog {
|
||||
return {
|
||||
id: page.id,
|
||||
title: page.properties.Title.title[0].plain_text,
|
||||
description: [
|
||||
page.properties.Description.rich_text.map(
|
||||
(d: { plain_text: string }) => d.plain_text
|
||||
),
|
||||
].join(" "),
|
||||
date: page.properties.Date.created_time,
|
||||
tags: page.properties.Tags.multi_select.map((tag: any) => ({
|
||||
name: tag.name,
|
||||
color: tag.color,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
interface FullBlog extends Blog {
|
||||
markdown: 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")
|
||||
return [400, { msg: "Blog not found" }];
|
||||
try {
|
||||
const blog = convertBlog(page);
|
||||
const mdBlocks = await n2m.pageToMarkdown(page.id);
|
||||
return [
|
||||
200,
|
||||
{
|
||||
id: blog.id,
|
||||
title: blog.title,
|
||||
description: blog.description,
|
||||
tags: blog.tags,
|
||||
date: blog.date,
|
||||
markdown: n2m.toMarkdownString(mdBlocks).parent || "",
|
||||
},
|
||||
];
|
||||
} catch (err: any) {
|
||||
return [500, { msg: String(err) }];
|
||||
}
|
||||
} catch {
|
||||
return [400, { msg: "Blog not found" }];
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchBlog(
|
||||
notion: Client,
|
||||
query: string
|
||||
): Promise<[200 | 400, QueryResponse | Error]> {
|
||||
try {
|
||||
var response = await notion.search({
|
||||
query,
|
||||
filter: {
|
||||
value: "page",
|
||||
property: "object",
|
||||
},
|
||||
sort: {
|
||||
direction: "ascending",
|
||||
timestamp: "last_edited_time",
|
||||
},
|
||||
});
|
||||
response.results = response.results.filter(
|
||||
(item: any) => item.properties.Status.status.name === "Posted"
|
||||
);
|
||||
return [
|
||||
200,
|
||||
{
|
||||
cursor: response.next_cursor,
|
||||
hasMore: response.has_more,
|
||||
items: response.results.map(convertBlog),
|
||||
},
|
||||
];
|
||||
} catch (err: any) {
|
||||
return [400, { msg: String(err) }];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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,3 +1,5 @@
|
||||
import { stringify } from "querystring";
|
||||
|
||||
type PinnedRepo = {
|
||||
author: string;
|
||||
name: string;
|
||||
@@ -133,24 +135,30 @@ export function Repo({
|
||||
}
|
||||
|
||||
export default async function PinnedRepos() {
|
||||
const pinned: PinnedRepo[] = await (
|
||||
await fetch("https://pinned.berrysauce.dev/get/kleo-dev")
|
||||
).json();
|
||||
try {
|
||||
const pinned: PinnedRepo[] = await (
|
||||
await fetch("https://pinned.berrysauce.dev/get/kleo-dev")
|
||||
).json();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 bg-transparent">
|
||||
{pinned.map((repo, i) => (
|
||||
<Repo
|
||||
key={i}
|
||||
author={repo.author}
|
||||
name={repo.name}
|
||||
description={repo.description}
|
||||
language={repo.language}
|
||||
stars={repo.stars}
|
||||
forks={repo.forks}
|
||||
href={`https://github.com/${repo.author}/${repo.name}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 bg-transparent">
|
||||
{pinned.map((repo, i) => (
|
||||
<Repo
|
||||
key={i}
|
||||
author={repo.author}
|
||||
name={repo.name}
|
||||
description={repo.description}
|
||||
language={repo.language}
|
||||
stars={repo.stars}
|
||||
forks={repo.forks}
|
||||
href={`https://github.com/${repo.author}/${repo.name}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} catch (e: any) {
|
||||
return (
|
||||
<p>{e.toString()}</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { Client } from '@notionhq/client';
|
||||
import { NotionToMarkdown } from 'notion-to-md';
|
||||
dotenv.config();
|
||||
|
||||
export const notion = new Client( { auth: process.env.NOTION_API_KEY } )
|
||||
export const n2m = new NotionToMarkdown({
|
||||
notionClient: notion,
|
||||
});
|
||||
|
||||
if (process.env.NOTION_DATABASE_ID === undefined) {
|
||||
console.error('[server] Error: environment variable \'NOTION_DATABASE_ID\' not found')
|
||||
process.exit();
|
||||
}
|
||||
export const databaseId: string = process.env.NOTION_DATABASE_ID;
|
||||
Reference in New Issue
Block a user