Skip to content
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
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "profile-dev",
"runtimeExecutable": "./node_modules/.bin/next",
"runtimeArgs": ["dev"],
"port": 3000
}
]
}
154 changes: 110 additions & 44 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import dompurify from 'isomorphic-dompurify';
import type {Metadata, NextPage} from 'next';
import AboutMeHero from '../../components/AboutMeHero';
import {BlurringImage} from '../../components/BlurringImage';
import NextLink from '../../components/basic/NextLink';
import CodingActivity from '../../components/home/CodingActivity';
import SectionLabel from '../../components/SectionLabel';
import {socialLinks} from '../../components/social/social-links';
import {newCommonMetaTags} from '../../frontend-utils/meta-tags';
import {getBlurringImage} from '../../utils/contentful.utils';
import {createTranslation} from '../i18n/server';
import {aboutMeData} from '@/models/static-content';

export async function generateMetadata(): Promise<Metadata> {
Expand All @@ -14,61 +17,124 @@ export async function generateMetadata(): Promise<Metadata> {
}

const AboutMePage: NextPage = async () => {
const {t} = await createTranslation('home');
const {t: tResume} = await createTranslation('resume');
const {
props: {aboutMeDetails, img, svg},
props: {paragraphs, img, svg},
} = await getStaticProps();

return (
<>
<AboutMeHero img={img} svg={svg} />
<main>
<article className="mb-20 pt-20" id="about-me-details">
<h1 className="text-2xl sm:text-3xl">About Carlo Gino Catapang</h1>
{aboutMeDetails.map(detail => {
return (
<section
className="flex flex-col items-center p-2 not-last:mb-4"
key={detail.key}
>
<h3 className="mb-4 text-xl">{detail.label}</h3>
<div
className="max-w-(--breakpoint-md) px-4 text-justify"
dangerouslySetInnerHTML={{
__html: dompurify.sanitize(detail.content),
}}
/>
</section>
);
})}
<main className="mx-auto max-w-6xl px-5">
<div className="grid gap-12 py-12 sm:py-16 lg:grid-cols-[minmax(0,1fr)_280px]">
<article aria-labelledby="about-title" className="max-w-[680px]">
<SectionLabel as="p">{t('about.kicker')}</SectionLabel>
<h1
id="about-title"
className="m-0 text-left text-3xl leading-tight font-semibold tracking-tight sm:text-4xl"
>
{t('about.title')}
</h1>
<p className="m-0 mt-4 text-lg text-balance text-ink-muted">
{tResume('tagline')}
</p>

<div className="mt-8 flex flex-col gap-5 text-[1.0625rem] leading-[1.75]">
{paragraphs.map(paragraph => (
<p key={paragraph} className="m-0 text-[length:inherit]">
{paragraph}
</p>
))}
</div>
</article>

<section className="pb-8">
<h4 className="text-center">
Here is the link to my&nbsp;
<NextLink
href="https://github.com/pogicodes/profile"
target="_blank"
aria-label="Github Repo"
className="text-primary-900 dark:text-primary-300"
>
Github Repo
</NextLink>
.
</h4>
</section>
</main>
</>
<aside className="space-y-10 lg:sticky lg:top-20 lg:self-start">
<div className="relative aspect-square w-40 overflow-hidden rounded-xl ring-1 ring-line lg:w-full">
<BlurringImage
img={img}
svg={svg}
alt="Carlo Gino Catapang"
title="Carlo Gino Catapang"
fill
sizes="(min-width: 1024px) 280px, 160px"
priority
className="object-cover"
/>
</div>

<section aria-labelledby="about-elsewhere">
<SectionLabel id="about-elsewhere">
{t('about.elsewhere')}
</SectionLabel>
<ul className="m-0 list-none space-y-1.5 p-0 font-mono text-sm">
{socialLinks.map(link => (
<li key={link.url}>
<a
href={link.url}
target="_blank"
rel="noreferrer"
className="text-sm lowercase hover:text-accent"
>
{link.label} ↗
</a>
</li>
))}
<li>
<a
href="mailto:[email protected]"
className="text-sm hover:text-accent"
>
email
</a>
</li>
</ul>
</section>

<section aria-labelledby="about-more">
<SectionLabel id="about-more">{t('about.seeAlso')}</SectionLabel>
<ul className="m-0 list-none space-y-1.5 p-0 font-mono text-sm">
<li>
<NextLink href="/resume" className="text-sm hover:text-accent">
~/resume
</NextLink>
</li>
<li>
<NextLink href="/blog" className="text-sm hover:text-accent">
~/blog
</NextLink>
</li>
<li>
<a
href="https://github.com/pogicodes/profile"
target="_blank"
rel="noreferrer"
className="text-sm lowercase hover:text-accent"
>
{t('about.source')} ↗
</a>
</li>
</ul>
</section>
</aside>
</div>

<CodingActivity />
</main>
);
};

const getStaticProps = async () => {
// Use local about me data instead of Contentful
const aboutMeDetails = aboutMeData.sort((a, b) => a.order - b.order);
// First-person bio; the third-person copy is kept in the data for reuse.
const bio = aboutMeData.find(item => item.key === 'about_me_long_1st_pov');
const paragraphs = (bio?.content ?? '')
.split(/\n\s*\n/)
.map(paragraph => paragraph.trim())
.filter(Boolean);

const {img, svg} = await getBlurringImage('batman_cover.png');
const {img, svg} = await getBlurringImage('profile_picture.jpg');

return {
props: {
aboutMeDetails,
paragraphs,
img,
svg,
},
Expand Down
128 changes: 77 additions & 51 deletions src/app/blog/BlogsWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,112 @@
'use client';
import {parseAsArrayOf, parseAsString, useQueryState} from 'next-usequerystate';
import {useMemo} from 'react';
import PostListItem from '../../components/blog/PostListItem';
import {IBlogMetadata} from '../../models/mdxFiles';
import BlogsFilter from '../../modules/blog/BlogsFilter';
import BlogCard from '../../modules/common/ContentCard';
import {useTranslation} from '../i18n/client';
import SearchField from './SearchField';

const groupByYear = (blogs: IBlogMetadata[]) =>
blogs.reduce<[string, IBlogMetadata[]][]>((groups, blog) => {
const year = String(new Date(blog.date).getFullYear());
const last = groups.at(-1);

if (last?.[0] === year) {
last[1].push(blog);
} else {
groups.push([year, [blog]]);
}

return groups;
}, []);

const BlogsWrapper = ({blogs}: {blogs: IBlogMetadata[]}) => {
const {t} = useTranslation('blog');
const [selectedTags, setSelectedTags] = useQueryState(
'tags',
parseAsArrayOf(parseAsString).withDefault([]),
);

const [search, setSearch] = useQueryState('search');

let filtered = blogs;

filtered = search
? blogs.filter(blog => {
if (
blog.title.toLowerCase().includes(search.toLowerCase()) ||
blog.description.toLowerCase().includes(search.toLowerCase())
) {
return true;
}
})
: blogs;

filtered = selectedTags.length
? blogs.filter(blog => selectedTags.every(tag => blog.tags.includes(tag)))
: filtered;
const query = search?.toLowerCase();
const filtered = blogs.filter(
blog =>
(!query ||
blog.title.toLowerCase().includes(query) ||
blog.description.toLowerCase().includes(query)) &&
selectedTags.every(tag => blog.tags.includes(tag)),
);

const tags = useMemo(() => {
const generatedTags = blogs.reduce((acc: Set<string>, blog) => {
blog.tags.forEach(tag => {
if (!acc.has(tag)) {
acc.add(tag);
}
});
const counts = new Map<string, number>();
blogs.forEach(blog =>
blog.tags.forEach(tag => counts.set(tag, (counts.get(tag) ?? 0) + 1)),
);

return acc;
}, new Set<string>());

return Array.from(generatedTags);
return [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.map(([tag]) => tag);
}, [blogs]);

return (
<>
<SearchField />
<br />
<BlogsFilter
tags={tags}
selectedTags={selectedTags}
handleTagClick={tag => {
setSelectedTags(prev => {
// If the tag is already selected, remove it from the selected tags
<div className="mt-8 space-y-3">
<SearchField />
<BlogsFilter
tags={tags}
selectedTags={selectedTags}
onClear={() => setSelectedTags(null)}
handleTagClick={tag => {
setSelectedTags(prev => {
if (prev.includes(tag)) {
return prev.length === 1 ? null : prev.filter(t => t !== tag);
}

if (prev.includes(tag)) {
return prev.length === 1 ? null : prev.filter(t => t !== tag);
}
return [...prev, tag];
});
}}
/>
</div>

// Otherwise, add it to the selected tags
return [...prev, tag];
});
}}
/>
{filtered.length > 0 ? (
filtered.map(blog => {
return <BlogCard key={blog.slug} blog={blog} />;
})
groupByYear(filtered).map(([year, posts], groupIndex) => (
<section
key={year}
aria-labelledby={`year-${year}`}
className="mt-10 border-t border-line pt-6"
>
<h2
id={`year-${year}`}
className="m-0 mb-4 text-left font-mono text-sm font-medium text-ink-muted"
>
{year}
</h2>
<ol className="m-0 list-none divide-y divide-line p-0">
{posts.map((blog, index) => (
<li key={blog.slug} className="py-6 first:pt-0">
<PostListItem
blog={blog}
priority={groupIndex === 0 && index === 0}
/>
</li>
))}
</ol>
</section>
))
) : (
<section className="flex flex-col items-center">
<h2>Empty result👎</h2>
<h3>Try a different combination</h3>
<section className="mt-10 border-t border-line pt-10 text-center">
<p className="m-0 text-ink-muted">{t('noResults')}</p>
<button
className="mt-4 rounded-md bg-primary-600 px-4 py-2 text-white hover:bg-primary-700"
type="button"
className="mt-4 rounded-md border border-line px-4 py-2 text-sm hover:bg-panel"
onClick={() => {
setSelectedTags(null);
setSearch(null);
}}
>
Reset
{t('reset')}
</button>
</section>
)}
Expand Down
Loading
Loading