switched to nextjs
This commit is contained in:
@@ -1,36 +1,3 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# `.env` requirements
|
||||||
|
- `NOTION_API_KEY`: Go to the notion developer portal
|
||||||
## Getting Started
|
- `NOTION_DATABASE_ID`: the database that the api will read from
|
||||||
|
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
Generated
+1564
-33
File diff suppressed because it is too large
Load Diff
+16
-6
@@ -9,14 +9,24 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.0.0-rc-66855b96-20241106",
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||||
"next": "15.0.3"
|
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
"@notionhq/client": "^2.2.15",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"grapheme-splitter": "^1.0.4",
|
||||||
|
"next": "15.0.3",
|
||||||
|
"notion-to-md": "^3.1.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-content-loader": "^7.0.2",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-loading-skeleton": "^3.5.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"typewriter-effect": "^2.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"typescript": "^5"
|
||||||
"@types/react-dom": "^18"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,104 @@
|
|||||||
|
@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: 'Roboto Mono';
|
||||||
|
color: #f5304c;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #f5304c;
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 100px 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: 600px;
|
||||||
|
text-align: left;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
user-select: text;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.blog_container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
@import url(https://fonts.googleapis.com/css?family=Roboto+Mono);
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Roboto);
|
||||||
|
|
||||||
|
h2, p {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
color: white;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #101010;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #f5304c;
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.home {
|
||||||
|
position: relative;
|
||||||
|
margin-top: -10%;
|
||||||
|
margin-left: 10%;
|
||||||
|
width: 45%;
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
margin-top: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.home {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 10%;
|
||||||
|
margin-left: 5%;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.logo {
|
||||||
|
width: 280px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 64%;
|
||||||
|
margin-top: 10%;
|
||||||
|
/* border: 3px solid #f5304c; */
|
||||||
|
/* outline: 10px solid #f5304c; */
|
||||||
|
/* outline-offset: 30px; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.logo {
|
||||||
|
width: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 64%;
|
||||||
|
margin-top: 10%;
|
||||||
|
/* border: 3px solid #f5304c; */
|
||||||
|
/* outline: 10px solid #f5304c; */
|
||||||
|
/* outline-offset: 30px; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech {
|
||||||
|
color: #f5304c;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
@media (orientation: portrait) {
|
||||||
|
.tech-stack {
|
||||||
|
position: absolute;
|
||||||
|
left: 53%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 86%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.tech-stack {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: 2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.shape-blob {
|
||||||
|
background:#f5304c;
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
border-radius: 30% 50% 20% 40%;
|
||||||
|
animation:
|
||||||
|
transform 20s ease-in-out infinite both alternate,
|
||||||
|
movement_one 40s ease-in-out infinite both;
|
||||||
|
opacity:.7;
|
||||||
|
position: absolute;
|
||||||
|
left: 70%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.shape-blob.one{
|
||||||
|
height: 500px;
|
||||||
|
width: 500px;
|
||||||
|
left: -200px;
|
||||||
|
top: -150px;
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
animation: transform 30s ease-in-out infinite both alternate, movement_two 60s ease-in-out infinite both;
|
||||||
|
}}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.shape-blob.two{
|
||||||
|
height: 350px;
|
||||||
|
width: 350px;
|
||||||
|
left: 500px;
|
||||||
|
top: -150px;
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
animation: transform 30s ease-in-out infinite both alternate, movement_two 60s ease-in-out infinite both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes transform
|
||||||
|
{
|
||||||
|
0%,
|
||||||
|
100% { border-radius: 33% 67% 70% 30% / 30% 30% 70% 70%; }
|
||||||
|
20% { border-radius: 37% 63% 51% 49% / 37% 65% 35% 63%; }
|
||||||
|
40% { border-radius: 36% 64% 64% 36% / 64% 48% 52% 36%; }
|
||||||
|
60% { border-radius: 37% 63% 51% 49% / 30% 30% 70% 70%; }
|
||||||
|
80% { border-radius: 40% 60% 42% 58% / 41% 51% 49% 59%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes movement_one
|
||||||
|
{
|
||||||
|
0%,
|
||||||
|
100% { transform: none; }
|
||||||
|
50% { transform: translate(50%, 20%) rotateY(10deg) scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes movement_two
|
||||||
|
{
|
||||||
|
0%,
|
||||||
|
500% { transform: none; }
|
||||||
|
50% { transform: translate(50%, 20%) rotate(-200deg) scale(1.2);}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { getBlog } from '../blog';
|
||||||
|
import { notion, n2m } from '../notion';
|
||||||
|
|
||||||
|
export async function GET(req: Request, { params }: { params: Promise<{ blogId: string }> }) {
|
||||||
|
let [status, data] = await getBlog(notion, n2m, (await params).blogId);
|
||||||
|
return Response.json(data, { status });
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Client } from '@notionhq/client';
|
||||||
|
import { NotionToMarkdown } from "notion-to-md";
|
||||||
|
|
||||||
|
interface Blog {
|
||||||
|
id: string,
|
||||||
|
title: string,
|
||||||
|
tags: string[],
|
||||||
|
date: string,
|
||||||
|
description: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function queryDatabase(notion: Client, databaseId: string): Promise<[Array<any>, any]> {
|
||||||
|
try {
|
||||||
|
const response = await notion.databases.query({
|
||||||
|
database_id: databaseId,
|
||||||
|
filter: {
|
||||||
|
property: "Status",
|
||||||
|
status: {
|
||||||
|
equals: "Posted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [response.results, null];
|
||||||
|
} catch (error: any) {
|
||||||
|
return [[], error];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function getBlogs(notion: Client, databaseId: string): Promise<[number, Blog[]]> {
|
||||||
|
const [pages, err] = await queryDatabase(notion, databaseId);
|
||||||
|
if (err === null) {
|
||||||
|
return [200, pages.map(convertBlog)];
|
||||||
|
}
|
||||||
|
return [500, 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) => (tag.name)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FullBlog extends Blog {
|
||||||
|
markdown: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBlog(notion: Client, n2m: NotionToMarkdown, pageId: string): Promise<[number, FullBlog]> {
|
||||||
|
try {
|
||||||
|
const page = await notion.pages.retrieve({ page_id: pageId })
|
||||||
|
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 (error: any) {
|
||||||
|
return [400, error];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ContentLoader from "react-content-loader"
|
||||||
|
|
||||||
|
export default function LoadingBlog(props: any) {
|
||||||
|
const [numComponents, setNumComponents] = useState(0);
|
||||||
|
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 (
|
||||||
|
<div className="container">
|
||||||
|
{Array.from({ length: numComponents }, (_, index) => (
|
||||||
|
<ContentLoader
|
||||||
|
speed={2}
|
||||||
|
viewBox="0 0 340 50"
|
||||||
|
backgroundColor="#3b3b3b"
|
||||||
|
foregroundColor="#f5304c"
|
||||||
|
key={index}
|
||||||
|
{...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 = () => {
|
||||||
|
// 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
|
||||||
|
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 (
|
||||||
|
<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} // Adjust position based on index
|
||||||
|
rx="3"
|
||||||
|
ry="3"
|
||||||
|
width={Math.random() * (300 - 80) + 80} // Random width
|
||||||
|
height="9"
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ContentLoader>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
|
||||||
|
export default function MarkdownRenderer({ children }: { children: string }) {
|
||||||
|
return <ReactMarkdown>{children}</ReactMarkdown>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import '../../Blog.css';
|
||||||
|
import MarkdownRenderer from './MarkdownRenderer';
|
||||||
|
import LoadingBlog, { LoadingBlogRead } from '../Loading';
|
||||||
|
|
||||||
|
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 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 <main>
|
||||||
|
<a href="/blog"><h1 className='name'>Leo's blog</h1></a>
|
||||||
|
<div className='blog_container'>
|
||||||
|
<LoadingBlogRead/>
|
||||||
|
</div>
|
||||||
|
</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="error">Error loading blog: {error}</div>; // Show error if there is one
|
||||||
|
}
|
||||||
|
|
||||||
|
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'>{date}</p>
|
||||||
|
<div className='markdown'><MarkdownRenderer>{blog.markdown}</MarkdownRenderer></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import '../Blog.css';
|
||||||
|
import LoadingBlog from './Loading';
|
||||||
|
|
||||||
|
export default function Blog() {
|
||||||
|
const [data, setData] = useState([{ title: '', description: '', id: '', date: '' }]);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/');
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
setData(result);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.text();
|
||||||
|
setError(errorData);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.toString());
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {JSON.stringify(error)}</div>; // Display any errors
|
||||||
|
} if (data.length === 1 && data[0].id === '') {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<a href="/blog"><h1 className='name'>Leo's blog</h1></a>
|
||||||
|
<LoadingBlog/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = "Leo's blog";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<a href="/blog"><h1 className='name'>Leo's blog</h1></a>
|
||||||
|
<div className='container'>
|
||||||
|
{data.map((blog, i) => {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
+3
-13
@@ -1,18 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import localFont from "next/font/local";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = localFont({
|
|
||||||
src: "./fonts/GeistVF.woff",
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
weight: "100 900",
|
|
||||||
});
|
|
||||||
const geistMono = localFont({
|
|
||||||
src: "./fonts/GeistMonoVF.woff",
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
weight: "100 900",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
@@ -25,7 +13,9 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
<title>Leo's portfolio</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico"></link>
|
||||||
|
<body>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
.page {
|
|
||||||
--gray-rgb: 0, 0, 0;
|
|
||||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
|
||||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
|
||||||
|
|
||||||
--button-primary-hover: #383838;
|
|
||||||
--button-secondary-hover: #f2f2f2;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 20px 1fr 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
min-height: 100svh;
|
|
||||||
padding: 80px;
|
|
||||||
gap: 64px;
|
|
||||||
font-family: var(--font-geist-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.page {
|
|
||||||
--gray-rgb: 255, 255, 255;
|
|
||||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
|
||||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
|
||||||
|
|
||||||
--button-primary-hover: #ccc;
|
|
||||||
--button-secondary-hover: #1a1a1a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 32px;
|
|
||||||
grid-row-start: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main ol {
|
|
||||||
font-family: var(--font-geist-mono);
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 24px;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main li:not(:last-of-type) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main code {
|
|
||||||
font-family: inherit;
|
|
||||||
background: var(--gray-alpha-100);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas a {
|
|
||||||
appearance: none;
|
|
||||||
border-radius: 128px;
|
|
||||||
height: 48px;
|
|
||||||
padding: 0 20px;
|
|
||||||
border: none;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition:
|
|
||||||
background 0.2s,
|
|
||||||
color 0.2s,
|
|
||||||
border-color 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.primary {
|
|
||||||
background: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary {
|
|
||||||
border-color: var(--gray-alpha-200);
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
grid-row-start: 3;
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer img {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enable hover only on non-touch devices */
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
|
||||||
a.primary:hover {
|
|
||||||
background: var(--button-primary-hover);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary:hover {
|
|
||||||
background: var(--button-secondary-hover);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.page {
|
|
||||||
padding: 32px;
|
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main ol {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas a {
|
|
||||||
font-size: 14px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.logo {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+63
-89
@@ -1,95 +1,69 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
import styles from "./page.module.css";
|
|
||||||
|
import './Page.css';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faGolang, faJs, faPython, faRust } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import GraphemeSplitter from 'grapheme-splitter';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const Typewriter = dynamic(() => import('typewriter-effect'), { ssr: false });
|
||||||
|
|
||||||
|
const stringSplitter = (text: string): any => {
|
||||||
|
const splitter = new GraphemeSplitter();
|
||||||
|
return splitter.splitGraphemes(text);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className="app">
|
||||||
<main className={styles.main}>
|
<img src="logo.png" alt="Logo" className="logo" />
|
||||||
<Image
|
<div className="shape-blob"></div>
|
||||||
className={styles.logo}
|
<div className="shape-blob one"></div>
|
||||||
src="/next.svg"
|
<div className="shape-blob two"></div>
|
||||||
alt="Next.js logo"
|
<div className="home">
|
||||||
width={180}
|
<h2>
|
||||||
height={38}
|
<Typewriter
|
||||||
priority
|
options={{
|
||||||
/>
|
stringSplitter,
|
||||||
<ol>
|
loop: true,
|
||||||
<li>
|
}}
|
||||||
Get started by editing <code>src/app/page.tsx</code>.
|
onInit={(typewriter) => {
|
||||||
</li>
|
typewriter
|
||||||
<li>Save and see your changes instantly.</li>
|
.typeString("Hey, I'm Leo 👋🏻")
|
||||||
</ol>
|
.start()
|
||||||
|
.pauseFor(2400)
|
||||||
|
.deleteAll()
|
||||||
|
.typeString("A software developer")
|
||||||
|
.pauseFor(2400)
|
||||||
|
.changeDeleteSpeed(10)
|
||||||
|
.deleteChars(18)
|
||||||
|
.typeString("Cybersecurity engineer")
|
||||||
|
.pauseFor(2400)
|
||||||
|
.deleteChars(22)
|
||||||
|
.typeString("Computer architect")
|
||||||
|
.pauseFor(2400);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
I'm a developer and researcher from Tirane, Albania. I'm the leader of{' '}
|
||||||
|
<a href="https://github.com/orus-dev">Orus</a>. I've been coding for 5
|
||||||
|
years and I'm in love with it. I work on free and open source projects.
|
||||||
|
I'm also willing to work on paid projects with a payment. You can find
|
||||||
|
me on <a href="https://instagram.com/kleo.dev">Instagram</a>,{' '}
|
||||||
|
<a href="https://github.com/kleo-dev">GitHub</a>,{' '}
|
||||||
|
<a href="https://www.youtube.com/@0xkleo">YouTube</a>, Or if you
|
||||||
|
prefer to contact me via Discord: leocodes. You can also check out{' '}
|
||||||
|
<a href="/blog">My Blog</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.ctas}>
|
<div className="tech-stack">
|
||||||
<a
|
<FontAwesomeIcon icon={faRust} className="tech" />
|
||||||
className={styles.primary}
|
<FontAwesomeIcon icon={faPython} className="tech" />
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
<FontAwesomeIcon icon={faGolang} className="tech" />
|
||||||
target="_blank"
|
<FontAwesomeIcon icon={faJs} className="tech" />
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className={styles.logo}
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={styles.secondary}
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user