Updated portfolio
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
@@ -38,3 +39,4 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -1,7 +1,36 @@
|
|||||||
# `.env` requirements
|
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).
|
||||||
- `NOTION_API_KEY`: Go to the notion developer portal
|
|
||||||
- `NOTION_DATABASE_ID`: the database that the api will read from
|
|
||||||
|
|
||||||
# API usage:
|
## Getting Started
|
||||||
`/`: lists all the posts,
|
|
||||||
`/<blogId>` gets the posts details and markdown
|
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
+1369
-3275
File diff suppressed because it is too large
Load Diff
+10
-28
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "portfolio-next",
|
"name": "portfolio",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,34 +9,16 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"react": "^19.0.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
"react-dom": "^19.0.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"next": "15.1.3"
|
||||||
"@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",
|
|
||||||
"prismjs": "^1.29.0",
|
|
||||||
"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",
|
|
||||||
"rehype-katex": "^7.0.1",
|
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-breaks": "^4.0.0",
|
|
||||||
"remark-footnotes": "^4.0.1",
|
|
||||||
"remark-gfm": "^4.0.0",
|
|
||||||
"remark-math": "^6.0.0",
|
|
||||||
"typewriter-effect": "^2.21.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.17.6",
|
"typescript": "^5",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/node": "^20",
|
||||||
"typescript": "^5"
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
@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);}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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 });
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Client } from '@notionhq/client';
|
|
||||||
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'}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { getBlogs } from './blog';
|
|
||||||
import { notion, databaseId } from './notion';
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+7
-23
@@ -1,3 +1,7 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
@@ -10,33 +14,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
max-width: 100vw;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
h1 {
|
||||||
box-sizing: border-box;
|
font-size: 30px;
|
||||||
padding: 0;
|
font-weight: bold;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -1,9 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
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"],
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Leo's portfolio",
|
title: "Leo's Portfolio",
|
||||||
description: "Leo's portfolio"
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -13,13 +25,10 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<title>Leo's portfolio</title>
|
<body
|
||||||
<link rel="icon" href="/favicon.ico" />
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png" />
|
>
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/logo.png" />
|
<Navbar />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/logo.png" />
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
<body>
|
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+14
-63
@@ -1,69 +1,20 @@
|
|||||||
"use client";
|
import Background from "@/components/Background";
|
||||||
|
import PinnedRepos from "@/components/PinnedRepos";
|
||||||
|
|
||||||
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 });
|
export default function App() {
|
||||||
|
|
||||||
const stringSplitter = (text: string): any => {
|
|
||||||
const splitter = new GraphemeSplitter();
|
|
||||||
return splitter.splitGraphemes(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div>
|
||||||
<img src="logo.png" alt="Logo" className="logo" />
|
<Background/>
|
||||||
<div className="shape-blob"></div>
|
<div className="content-center mx-auto sm:w-[900px]">
|
||||||
<div className="shape-blob one"></div>
|
<h1 className="py-2">Hey, I'm Leo 👋🏻</h1>
|
||||||
<div className="shape-blob two"></div>
|
<p className="font-semibold">A passionate software enginner in Albania. I build full stack web apps but mainly desktop apps and TUI apps.</p>
|
||||||
<div className="home">
|
<h1 className="py-3 text-red-400">Pinned Projects</h1>
|
||||||
<h2>
|
<section className="w-full max-w-4xl mb-10 px-6 sm:px-0">
|
||||||
<Typewriter
|
<PinnedRepos />
|
||||||
options={{
|
</section>
|
||||||
stringSplitter,
|
<footer className="text-red-400 flex w-full prose absolute bottom-0 py-4 lg:px-0 md:px-8 px-8"><p className="m-0 text-sm"><a className="text-red hover:text-red-400 duration-300 underline decoration-dotted underline-offset-4" target="_blank" rel="noreferrer" href="https://opensource.org/license/apache-2-0">Apache-2.0</a> © <a className="text-red hover:text-red-400 duration-300 underline decoration-dotted underline-offset-4" target="_blank" rel="noreferrer" href="https://github.com/kleo-dev">Kleo</a></p></footer>
|
||||||
loop: true,
|
|
||||||
}}
|
|
||||||
onInit={(typewriter) => {
|
|
||||||
typewriter
|
|
||||||
.typeString("Hey, I'm Leo 👋🏻")
|
|
||||||
.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="tech-stack">
|
|
||||||
<FontAwesomeIcon icon={faRust} className="tech" />
|
|
||||||
<FontAwesomeIcon icon={faPython} className="tech" />
|
|
||||||
<FontAwesomeIcon icon={faGolang} className="tech" />
|
|
||||||
<FontAwesomeIcon icon={faJs} className="tech" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
@keyframes move {
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 0, 1px) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #000000;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background span {
|
||||||
|
width: 24vmin;
|
||||||
|
height: 24vmin;
|
||||||
|
border-radius: 24vmin;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
animation: move;
|
||||||
|
animation-duration: 6;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.background span:nth-child(0) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 24%;
|
||||||
|
left: 18%;
|
||||||
|
animation-duration: 220s;
|
||||||
|
animation-delay: -272s;
|
||||||
|
transform-origin: -5vw -4vh;
|
||||||
|
box-shadow: 48vmin 0 6.421535280295322vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(1) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 86%;
|
||||||
|
left: 77%;
|
||||||
|
animation-duration: 91s;
|
||||||
|
animation-delay: -322s;
|
||||||
|
transform-origin: 14vw 0vh;
|
||||||
|
box-shadow: -48vmin 0 6.323515398437237vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(2) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 17%;
|
||||||
|
left: 44%;
|
||||||
|
animation-duration: 248s;
|
||||||
|
animation-delay: -181s;
|
||||||
|
transform-origin: 0vw -7vh;
|
||||||
|
box-shadow: -48vmin 0 6.116084464270985vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(3) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 43%;
|
||||||
|
left: 15%;
|
||||||
|
animation-duration: 98s;
|
||||||
|
animation-delay: -211s;
|
||||||
|
transform-origin: -21vw 8vh;
|
||||||
|
box-shadow: 48vmin 0 6.907246782805462vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(4) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 66%;
|
||||||
|
left: 60%;
|
||||||
|
animation-duration: 241s;
|
||||||
|
animation-delay: -75s;
|
||||||
|
transform-origin: 15vw -9vh;
|
||||||
|
box-shadow: -48vmin 0 6.832728853871303vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(5) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 79%;
|
||||||
|
left: 87%;
|
||||||
|
animation-duration: 207s;
|
||||||
|
animation-delay: -234s;
|
||||||
|
transform-origin: -19vw 4vh;
|
||||||
|
box-shadow: -48vmin 0 6.062609002101319vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(6) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 45%;
|
||||||
|
left: 79%;
|
||||||
|
animation-duration: 243s;
|
||||||
|
animation-delay: -285s;
|
||||||
|
transform-origin: 11vw 25vh;
|
||||||
|
box-shadow: -48vmin 0 6.42616777258573vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(7) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 43%;
|
||||||
|
left: 51%;
|
||||||
|
animation-duration: 227s;
|
||||||
|
animation-delay: -181s;
|
||||||
|
transform-origin: -23vw 1vh;
|
||||||
|
box-shadow: 48vmin 0 6.831515741167314vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(8) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 21%;
|
||||||
|
left: 59%;
|
||||||
|
animation-duration: 228s;
|
||||||
|
animation-delay: -196s;
|
||||||
|
transform-origin: -23vw 23vh;
|
||||||
|
box-shadow: 48vmin 0 6.824335114512845vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(9) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 95%;
|
||||||
|
left: 81%;
|
||||||
|
animation-duration: 202s;
|
||||||
|
animation-delay: -141s;
|
||||||
|
transform-origin: 17vw 24vh;
|
||||||
|
box-shadow: 48vmin 0 6.5732268320821685vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(10) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 23%;
|
||||||
|
left: 90%;
|
||||||
|
animation-duration: 43s;
|
||||||
|
animation-delay: -406s;
|
||||||
|
transform-origin: -7vw -12vh;
|
||||||
|
box-shadow: 48vmin 0 6.1955583991481vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(11) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 6%;
|
||||||
|
left: 94%;
|
||||||
|
animation-duration: 422s;
|
||||||
|
animation-delay: -38s;
|
||||||
|
transform-origin: -5vw 16vh;
|
||||||
|
box-shadow: 48vmin 0 6.212234588070589vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(12) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 76%;
|
||||||
|
left: 22%;
|
||||||
|
animation-duration: 258s;
|
||||||
|
animation-delay: -58s;
|
||||||
|
transform-origin: -23vw -6vh;
|
||||||
|
box-shadow: 48vmin 0 6.165111619626152vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(13) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 20%;
|
||||||
|
left: 80%;
|
||||||
|
animation-duration: 363s;
|
||||||
|
animation-delay: -411s;
|
||||||
|
transform-origin: 22vw 25vh;
|
||||||
|
box-shadow: -48vmin 0 6.179675889584371vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(14) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 88%;
|
||||||
|
left: 36%;
|
||||||
|
animation-duration: 12s;
|
||||||
|
animation-delay: -69s;
|
||||||
|
transform-origin: -11vw -4vh;
|
||||||
|
box-shadow: -48vmin 0 6.07188338021629vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(15) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 97%;
|
||||||
|
left: 11%;
|
||||||
|
animation-duration: 351s;
|
||||||
|
animation-delay: -130s;
|
||||||
|
transform-origin: -16vw -18vh;
|
||||||
|
box-shadow: 48vmin 0 6.558182158209241vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(16) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 60%;
|
||||||
|
left: 99%;
|
||||||
|
animation-duration: 193s;
|
||||||
|
animation-delay: -67s;
|
||||||
|
transform-origin: 10vw 25vh;
|
||||||
|
box-shadow: 48vmin 0 6.570051729134467vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(17) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 12%;
|
||||||
|
left: 8%;
|
||||||
|
animation-duration: 166s;
|
||||||
|
animation-delay: -69s;
|
||||||
|
transform-origin: 7vw -1vh;
|
||||||
|
box-shadow: -48vmin 0 6.742868690500271vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(18) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 44%;
|
||||||
|
left: 38%;
|
||||||
|
animation-duration: 96s;
|
||||||
|
animation-delay: -279s;
|
||||||
|
transform-origin: 11vw 1vh;
|
||||||
|
box-shadow: -48vmin 0 6.261014277195781vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(19) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 96%;
|
||||||
|
left: 77%;
|
||||||
|
animation-duration: 122s;
|
||||||
|
animation-delay: -355s;
|
||||||
|
transform-origin: 6vw 3vh;
|
||||||
|
box-shadow: -48vmin 0 6.438018625575824vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(20) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 2%;
|
||||||
|
left: 81%;
|
||||||
|
animation-duration: 331s;
|
||||||
|
animation-delay: -355s;
|
||||||
|
transform-origin: 11vw -18vh;
|
||||||
|
box-shadow: -48vmin 0 6.555921470976377vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(21) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 42%;
|
||||||
|
left: 9%;
|
||||||
|
animation-duration: 439s;
|
||||||
|
animation-delay: -233s;
|
||||||
|
transform-origin: -20vw -24vh;
|
||||||
|
box-shadow: -48vmin 0 6.570997236391078vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(22) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 69%;
|
||||||
|
left: 23%;
|
||||||
|
animation-duration: 400s;
|
||||||
|
animation-delay: -390s;
|
||||||
|
transform-origin: -18vw -21vh;
|
||||||
|
box-shadow: 48vmin 0 6.279087258882428vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(23) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 33%;
|
||||||
|
left: 95%;
|
||||||
|
animation-duration: 18s;
|
||||||
|
animation-delay: -98s;
|
||||||
|
transform-origin: 22vw 8vh;
|
||||||
|
box-shadow: -48vmin 0 6.088381708804739vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(24) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 39%;
|
||||||
|
left: 69%;
|
||||||
|
animation-duration: 337s;
|
||||||
|
animation-delay: -83s;
|
||||||
|
transform-origin: 15vw -5vh;
|
||||||
|
box-shadow: -48vmin 0 6.515991392427649vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(25) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 57%;
|
||||||
|
left: 17%;
|
||||||
|
animation-duration: 126s;
|
||||||
|
animation-delay: -93s;
|
||||||
|
transform-origin: -7vw 9vh;
|
||||||
|
box-shadow: 48vmin 0 6.431063119719202vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(26) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 91%;
|
||||||
|
left: 76%;
|
||||||
|
animation-duration: 131s;
|
||||||
|
animation-delay: -256s;
|
||||||
|
transform-origin: -4vw 23vh;
|
||||||
|
box-shadow: 48vmin 0 6.370776053425152vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(27) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 44%;
|
||||||
|
left: 65%;
|
||||||
|
animation-duration: 160s;
|
||||||
|
animation-delay: -235s;
|
||||||
|
transform-origin: -23vw 12vh;
|
||||||
|
box-shadow: 48vmin 0 6.34935032141241vmin currentColor;
|
||||||
|
}
|
||||||
|
.background span:nth-child(28) {
|
||||||
|
color: #f5304c;
|
||||||
|
top: 89%;
|
||||||
|
left: 36%;
|
||||||
|
animation-duration: 298s;
|
||||||
|
animation-delay: -400s;
|
||||||
|
transform-origin: -7vw 2vh;
|
||||||
|
box-shadow: -48vmin 0 6.935061205772426vmin currentColor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import './Background.css';
|
||||||
|
|
||||||
|
const Background = () => {
|
||||||
|
return (
|
||||||
|
<div className="background blur-2xl">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Background;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const Navbar = () => {
|
||||||
|
return (
|
||||||
|
<nav className="p-4 flex items-center justify-center">
|
||||||
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
className="space-x-4 text-white block font-bold"
|
||||||
|
>
|
||||||
|
<a href="/" className="hover:text-red transition-colors duration-300">
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
<a href="/contact" className="hover:text-red transition-colors duration-300">
|
||||||
|
Contact
|
||||||
|
</a>
|
||||||
|
<a href="/services" className="hover:text-red transition-colors duration-300">
|
||||||
|
Pricing
|
||||||
|
</a>
|
||||||
|
<a href="/blog" className="hover:text-red transition-colors duration-300">
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
type PinnedRepo = {
|
||||||
|
author: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
language: string,
|
||||||
|
stars: number,
|
||||||
|
forks: number,
|
||||||
|
href?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Repo({name, description: desc, language: lang, stars, forks, href}: PinnedRepo) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
group
|
||||||
|
relative
|
||||||
|
flex
|
||||||
|
size-full
|
||||||
|
overflow-hidden
|
||||||
|
rounded-xl
|
||||||
|
h-full border
|
||||||
|
border-red
|
||||||
|
hover:shadow-md
|
||||||
|
transition-shadow
|
||||||
|
duration-300
|
||||||
|
bg-transparent
|
||||||
|
backdrop-blur-xl
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="rounded-xl border text-card-foreground shadow h-full bg-transparent border-none flex flex-col">
|
||||||
|
<div className="flex flex-col space-y-1.5 p-6 flex-grow">
|
||||||
|
<h3 className="tracking-tight text-base sm:text-lg font-semibold text-red-400">
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="hover:underline"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-400 line-clamp-2 mt-1">
|
||||||
|
{desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 pt-0 mt-auto">
|
||||||
|
<div className="flex flex-wrap justify-between items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2 sm:gap-4">
|
||||||
|
<div className="flex items-center gap-1 text-xs sm:text-sm" data-state="closed">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="lucide lucide-star text-yellow-500 h-3 w-3 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-gray-400">{stars}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs sm:text-sm" data-state="closed">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="lucide lucide-git-fork text-gray-500 h-3 w-3 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="18" r="3" />
|
||||||
|
<circle cx="6" cy="6" r="3" />
|
||||||
|
<circle cx="18" cy="6" r="3" />
|
||||||
|
<path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" />
|
||||||
|
<path d="M12 12v3" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-gray-400">{forks}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold 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" data-sentry-component="Badge" data-sentry-source-file="badge.tsx">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="lucide lucide-code-xml h-3 w-3 mr-1"
|
||||||
|
>
|
||||||
|
<path d="m18 16 4-4-4-4" />
|
||||||
|
<path d="m6 8-4 4 4 4" />
|
||||||
|
<path d="m14.5 4-5 16" />
|
||||||
|
</svg>
|
||||||
|
{lang}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PinnedRepos() {
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "var(--background)",
|
||||||
|
foreground: "var(--foreground)",
|
||||||
|
red: '#f5304c',
|
||||||
|
'red-400': '#fa5770',
|
||||||
|
'red-600': '#eb2642',
|
||||||
|
'transparent-ish': '#0000000F'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
||||||
Reference in New Issue
Block a user