Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POR-49] Create blog page #70

Merged
merged 12 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/blog/[slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactNode } from 'react';

export default function Layout({ children }: { children: ReactNode }) {
return (
<section className="mx-auto w-full max-w-2xl space-y-48 md:space-y-32 print:space-y-6 animate-fade-in-left delay-500">
{children}
</section>
);
}
93 changes: 93 additions & 0 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { getPost, getPosts } from '@/data/blog';
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote/rsc';
import remarkGfm from 'remark-gfm';
import remarkFrontmatter from 'remark-frontmatter';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import remarkToc from 'remark-toc';
import { mdxComponents } from '@/components/mdx/components';
import { ArrowLeftIcon, CalendarIcon } from '@radix-ui/react-icons';
import Link from 'next/link';
import { data } from '@/data/main';
import rehypePrettyCode from 'rehype-pretty-code';

export async function generateMetadata(props: {
params: Promise<{
slug: string;
}>;
}) {
const params = await props.params;
const post = await getPost(params.slug);
return {
title: `${data.name} | ${data.role} :: ${post?.title}`,
description: post?.description + ' ' + data.summary,
alternates: {
canonical: `https://maria-adriana.com/blog/${post?.slug}`
}
};
}

export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage(props: {
params: Promise<{
slug: string;
}>;
}) {
const params = await props.params;

const post = await getPost(params.slug);

if (!post) {
return notFound();
}

const { title, description, date, body } = post;

return (
<article className="flex flex-col gap-2 blog-page">
<Link
href="/blog"
className="flex flex-row gap-2 font-mono text-sm text-gray-500 dark:text-white/60 mb-8 items-center hover:underline hover:text-orange-500 hover:opacity-75 duration-200"
>
<span className="flex border border-gray-300 dark:border-white/20 bg-card rounded-full h-7 w-7 items-center justify-center">
<ArrowLeftIcon className="text-gray-500 dark:text-white/80" />
</span>
Go back
</Link>
<h1 className="font-clash">{title}</h1>
<p className="font-mono text-sm text-foreground line-clamp-3">{description}</p>
<time className="flex flex-row gap-2 items-center text-pretty font-mono text-xs text-foreground text-gray-500 dark:text-gray-300">
<CalendarIcon />
{date}
</time>
<hr className="mt-4" />
<div className="content">
<MDXRemote
source={body}
options={{
mdxOptions: {
remarkPlugins: [
remarkGfm,
remarkFrontmatter,
[
remarkToc,
{
tight: true,
maxDepth: 5
}
]
],
rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings, rehypePrettyCode]
}
}}
components={mdxComponents}
/>
</div>
</article>
);
}
31 changes: 15 additions & 16 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { Button } from '@/components/ui/button';
import PostsList from '@/components/pages/blog/posts-list';
import { getPosts } from '@/data/blog';
import { data } from '@/data/main';
import { Metadata } from 'next';
import Link from 'next/link';

export const metadata: Metadata = {
title: `${data.name} | ${data.about}`,
description: data.summary
title: `${data.name} | ${data.role} :: Blog`,
description: 'My personal blog, for rambles and so on → ' + data.summary,
alternates: {
canonical: 'https://maria-adriana.com/blog'
}
};

export default function Page() {
return (
<div className="flex flex-col justify-center items-center h-80">
<h2 className="text-4xl font-semibold">Coming soon.</h2>
<p className="mt-4">🚧 Page under construction 🚧</p>
<div className="flex flex-col mx-auto mt-10 gap-9">
<Button variant="outline" size="lg" asChild>
<Link href="/">Return Home</Link>
</Button>
</div>
</div>
);
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}

export default async function Page() {
const posts = await getPosts();
return <PostsList posts={posts} />;
}
50 changes: 32 additions & 18 deletions app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import { clashDisplay } from '@/app/layout';
import { ContactForm } from '@/components/pages/contact/form';
import { Section } from '@/components/ui/section';
import { data } from '@/data/main';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: `${data.name} | ${data.role} :: Contact`,
description: 'Contact me → ' + data.summary,
alternates: {
canonical: 'https://maria-adriana.com/contact'
}
};

export default function Page() {
return (
<Section className="mt-20 animate-fade-in-left">
<div className="flex flex-col justify-center items-center mt-20">
<div className="flex flex-col items-center align-middle self-center max-w-2xl w-full gap-10">
<div className="flex flex-col w-full gap-4">
<h2 className={`${clashDisplay.className} text-xl font-bold`}>Reach out</h2>
<p className="text-pretty font-mono text-sm text-foreground leading-5">
I'm always interested in new projects and opportunities. Feel free to
get in touch with me if you have a project in mind, want to discuss a
potential collaboration, or simply want to say hello. You can reach me
through the contact form below.
<br />
Typically I respond back to emails within
<span className="text-orange-600">1 business day</span>.
</p>
<>
<header className="mx-auto w-full max-w-2xl space-y-2 animate-fade-in-left delay-500 mb-20">
<h1 className="font-clash font-bold text-5xl text-fade-grad">Contact</h1>
<h4 className="font-clash font-medium text-md text-gray-500">Reach out</h4>
<p className="pb-10 text-pretty font-mono text-sm text-foreground leading-5">
I'm always interested in new projects and opportunities. Feel free to get in
touch with me if you have a project in mind, want to discuss a potential
collaboration, or simply want to say hello. You can reach me through the contact
form below.
<br />
Typically I respond back to emails within{' '}
<span className="text-orange-600">1 business day</span>.
</p>
<hr />
</header>
<Section className="mt-20 animate-fade-in-left delay-700">
<div className="flex flex-col justify-center items-center mt-20">
<div className="flex flex-col items-center align-middle self-center max-w-2xl w-full gap-10">
<div className="flex flex-col w-full gap-4"></div>
<ContactForm />
</div>
<ContactForm />
</div>
</div>
</Section>
</Section>
</>
);
}
15 changes: 14 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { BGGrid } from '@/components/bg-grid';
import '@/styles/globals.css';
import { data } from '@/data/main';
import Script from 'next/script';
import ProgressIndicator from '@/components/progress-indicator';
import { getPosts } from '@/data/blog';

export const clashDisplay = localFont({
src: [
Expand Down Expand Up @@ -43,7 +45,16 @@ export const clashDisplay = localFont({
]
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const postsLinks = await getPosts().then((posts) => {
return posts.map((post) => {
return {
url: `/blog/${post.slug}`,
title: post.title,
type: 'blog'
};
});
});
return (
<html lang="en" className="max-w-full overflow-y-scroll overflow-x-hidden no-scrollbar">
<Head title={`${data.name} | ${data.role}`} url={`${process.env.NEXT_PUBLIC_URL}`} />
Expand All @@ -67,6 +78,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
/>
<ThemeProvider attribute="class" defaultTheme="dark">
<Navbar />
<ProgressIndicator />
<main className="container relative mx-auto mt-28 overflow-auto print:p-12 overflow-y-scroll overflow-x-hidden no-scrollbar">
<BGGrid>{children}</BGGrid>
<CommandMenu
Expand All @@ -81,6 +93,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
title: 'Contact',
type: 'internal'
},
...postsLinks,
...data.contact.social.map((socialMediaLink) => ({
url: socialMediaLink.url,
title: socialMediaLink.name,
Expand Down
18 changes: 9 additions & 9 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { Button } from '@/components/ui/button';

export default function NotFound() {
return (
<div className="flex justify-center items-center">
<div className="mx-auto mt-10">
<h2 className="text-4xl font-semibold">Not Found</h2>
<p className="mt-4">Could not find requested resource</p>

<Button variant="outline" size="lg" asChild className="mt-10">
<Link href="/">Return Home</Link>
</Button>
</div>
<div className="mx-auto text-center max-w-60 mt-10 flex flex-col items-center justify-center">
<h2 className="text-fade-grad font-clash text-4xl font-semibold">Not found :'(</h2>
<p className="mt-4">
Whatever you were looking for, is simply not here.{' '}
<i>Have you tried looking for under the bed?</i>
</p>
<Button variant="outline" size="lg" asChild className="mt-10">
<Link href="/">Feeling lucky ✨</Link>
</Button>
</div>
);
}
4 changes: 2 additions & 2 deletions components/bg-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Image from 'next/image';

export function BGGrid({ children }: { children?: React.ReactNode }) {
return (
<div className="w-full relative">
<>
{children}
<div
className="fixed inset-0 z-[-1] bg-transparent h-screen w-screen bg-gradient-to-b from-muted to-background"
Expand Down Expand Up @@ -37,6 +37,6 @@ export function BGGrid({ children }: { children?: React.ReactNode }) {
/>
<div className="h-screen fixed border-l-[1px] border-orange-600 opacity-30 left-[5px] md:left-[10%] top-0 bottom-0" />
<div className="h-screen fixed border-l-[1px] border-orange-600 opacity-30 right-[5px] md:right-[10%] top-0 bottom-0" />
</div>
</>
);
}
25 changes: 20 additions & 5 deletions components/command-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import * as React from 'react';
import { useState, useEffect } from 'react';
import {
CommandDialog,
CommandEmpty,
Expand All @@ -20,12 +20,13 @@ interface Props {
}

export const CommandMenu = ({ links }: Props) => {
const [open, setOpen] = React.useState(false);
const [open, setOpen] = useState(false);

const socialsLinks = links.filter((link) => link.type === 'social');
const internalLinks = links.filter((link) => link.type === 'internal');
const blogLinks = links.filter((link) => link.type === 'blog');
const socialsLinks = links.filter((link) => link.type === 'social');

React.useEffect(() => {
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
Expand Down Expand Up @@ -55,7 +56,21 @@ export const CommandMenu = ({ links }: Props) => {
key={url}
onSelect={() => {
setOpen(false);
window.open(url);
window.location.href = url;
}}
>
<span>{title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Blog">
{blogLinks.map(({ url, title }) => (
<CommandItem
key={url}
onSelect={() => {
setOpen(false);
window.location.href = url;
}}
>
<span>{title}</span>
Expand Down
32 changes: 32 additions & 0 deletions components/mdx/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DetailedHTMLProps, HTMLAttributes } from 'react';
import NextImage from 'next/image';
import { MDXComponents } from 'mdx/types';
import { MDXNote } from './mdx-note';
import { MDXImage } from './mdx-image';
import { HomeIcon, InfoCircledIcon } from '@radix-ui/react-icons';

export const mdxComponents: MDXComponents = {
pre: ({
children,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLPreElement>) => {
return <code {...props}>{children}</code>;
},
img: MDXImage,
Image: NextImage,
Details: ({
children,
summary,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLDetailsElement> & {
summary: string;
}) => (
<details {...props}>
{summary && <summary>{summary}</summary>}
{children}
</details>
),
Note: MDXNote,
InfoIcon: InfoCircledIcon,
HomeIcon: HomeIcon
};
30 changes: 30 additions & 0 deletions components/mdx/components/mdx-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import NextImage from 'next/image';
import { DetailedHTMLProps, ImgHTMLAttributes } from 'react';

export function MDXImage({
src,
alt
}: DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> & {
src: string;
alt: string;
}) {
let widthFromSrc, heightFromSrc;
const url = new URL(src, 'https://maria-adriana.com');
const widthParam = url.searchParams.get('w') || url.searchParams.get('width');
const heightParam = url.searchParams.get('h') || url.searchParams.get('height');
if (widthParam) {
widthFromSrc = parseInt(widthParam);
}
if (heightParam) {
heightFromSrc = parseInt(heightParam);
}

const imageProps = {
src,
alt,
height: heightFromSrc || 450,
width: widthFromSrc || 550
};

return <NextImage {...imageProps} />;
}
Loading