Skip to content

Commit

Permalink
Merge pull request #8 from SubstantialCattle5/dev
Browse files Browse the repository at this point in the history
feat: blog over view page
  • Loading branch information
SubstantialCattle5 authored Oct 18, 2023
2 parents 7ce5133 + c529267 commit 9f1f1f1
Show file tree
Hide file tree
Showing 19 changed files with 2,492 additions and 41 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,25 @@
"cloudinary-build-url": "^0.2.4",
"clsx": "^1.2.1",
"date-fns": "^2.30.0",
"fs": "^0.0.1-security",
"lodash": "^4.17.21",
"lucide-react": "^0.260.0",
"mdx-bundler": "^9.2.1",
"next": "^13.5.4",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.5.2",
"react-lite-youtube-embed": "^2.3.52",
"react-tippy": "^1.4.0",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.0.0",
"rehype-pretty-code": "^0.10.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"shiki": "^0.14.5",
"swr": "^2.2.4",
"tailwind-merge": "^1.14.0",
"typewriter-effect": "^2.21.0"
Expand All @@ -46,6 +55,7 @@
"@commitlint/config-conventional": "^16.2.4",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@types/react": "^18.2.25",
Expand Down
17 changes: 9 additions & 8 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const Page = () => {
alt='Photo of me'
preview={false}
/>
<article className='prose dark:prose-invert'>
<article className=''>
<p data-fade='3'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Vestibulum auctor ex vel nibh tempus, id suscipit mauris
Expand All @@ -63,18 +63,19 @@ const Page = () => {
ces posuere cubilia Curae; Curabitur semper finibus justo, id
dictum tellus commodo vitae. Suspendisse potenti. Quisque et
sodales lectus. Suspendisse euismod leo eu tincidunt luctus.
Fusce dictum, odio vel ullamcorper cursus,
Fusce dictum, odio vel ullamcorper cursus, elit odio fermentum
ex, nec interdum orci felis et ligula.
</p>
<br />
<p data-fade='5'>
elit odio fermentum ex, nec interdum orci felis et ligula. Sed
volutpat massa nec tortor bibendum, non interdum purus
malesuada. Fusce eu risus vitae dolor iaculis malesuada.
Quisque id commodo purus. Nam vel ligula auctor, bibendum
ligula in, consectetur dolor.
ces posuere cubilia Curae; Curabitur semper finibus justo, id
dictum tellus commodo vitae. Suspendisse potenti. Quisque et
sodales lectus. Suspendisse euismod leo eu tincidunt luctus.
Fusce dictum, odio vel ullamcorper cursus, elit odio fermentum
ex, nec interdum orci felis et ligula.
</p>
</article>
<h3 className='mt-6'>Current Favourite Stack:</h3>
<h3 className='mt-12'>Current Favourite Stack:</h3>
<figure className='mt-2'>
<TechStack />
</figure>
Expand Down
5 changes: 5 additions & 0 deletions src/app/api/blog/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';

export async function POST() {
return NextResponse.json({ hello: 'Next.js' });
}
134 changes: 134 additions & 0 deletions src/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client';
import clsx from 'clsx';
import React from 'react';

import { getTags, sortByDate, sortDateFn } from '@/lib/mdx.client';
import useInjectContentMeta from '@/hooks/useInjectContentMeta';
import useLoaded from '@/hooks/useLoaded';

import Accent from '@/components/Accent';
import BlogCard from '@/components/content/blogs/BlogCard';
import ContentPlaceholder from '@/components/content/ContenPlaceholder';
import StyledInput from '@/components/content/form/StyledInput';
import Tag, { SkipNavTag } from '@/components/content/Tag';

import { BlogFrontmatter } from '@/types/frontmatters';

const Page = () => {
const isLoaded = useLoaded();
const popluatedPosts = useInjectContentMeta('blog', 'featuredBlogs');

//#region //* Search
const posts = sortByDate(popluatedPosts as BlogFrontmatter[]);
const [search, setSearch] = React.useState<string>('');
const [filteredPosts, setFilteredPosts] = React.useState<
Array<BlogFrontmatter>
>(() => [...posts]);

const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};

React.useEffect(() => {
const jugglePosts = popluatedPosts as BlogFrontmatter[];
const results = jugglePosts.filter(
(post) =>
post.title.toLowerCase().includes(search.toLowerCase()) ||
post.description.toLowerCase().includes(search.toLowerCase()) ||
// Check if splitted search contained in tag
search
.toLowerCase()
.split(' ')
.every((tag) => post.tags.includes(tag))
);
results.sort(sortDateFn);
setFilteredPosts(results);
}, [search, popluatedPosts]);

//#region //* end Search
const currentPosts = filteredPosts;

//#region //* Tag
const tags = getTags(popluatedPosts as BlogFrontmatter[]);
const toggleTag = (tag: string) => {
if (search.includes(tag)) {
setSearch((s) =>
s
.split(' ')
.filter((t) => t !== tag)
?.join(' ')
);
} else {
// append tag
setSearch((s) => (s !== '' ? `${s.trim()} ${tag}` : tag));
}
};
/** Currently available tags based on filtered posts */
const filteredTags = getTags(currentPosts);

/** Show accent if not disabled and selected */
const checkTagged = (tag: string) => {
return (
filteredTags.includes(tag) &&
search.toLowerCase().split(' ').includes(tag)
);
};
//#region //* end Tag

return (
<>
<main>
<section className={clsx(isLoaded && 'fade-in-start')}>
<div className='layout py-12'>
<h1 className='text-3xl md:text-5xl' data-fade='0'>
<Accent>Blog </Accent>
</h1>
<p className='mt-2 text-gray-600 dark:text-gray-300' data-fade='1'>
Thoughts, mental models, and tutorials about front-end
development.
</p>
<StyledInput
data-fade='2'
className='mt-4'
placeholder='Search...'
onChange={handleSearch}
value={search}
type='text'
/>
<div
className='mt-2 flex flex-wrap items-baseline justify-start gap-2 text-sm text-gray-600 dark:text-gray-300'
data-fade='3'
>
<span className='font-medium'>Choose topic:</span>
<SkipNavTag>
{tags.map((tag) => (
<Tag
key={tag}
onClick={() => toggleTag(tag)}
disabled={!filteredTags.includes(tag)}
>
{checkTagged(tag) ? <Accent>{tag}</Accent> : tag}
</Tag>
))}
</SkipNavTag>
</div>
<ul
className='mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-3'
data-fade='5'
>
{currentPosts.length > 0 ? (
currentPosts.map((post) => (
<BlogCard key={post.slug} post={post} />
))
) : (
<ContentPlaceholder />
)}
</ul>
</div>
</section>
</main>
</>
);
};

export default Page;
9 changes: 9 additions & 0 deletions src/components/content/ContenPlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Accent from '@/components/Accent';

export default function ContentPlaceholder() {
return (
<h2 className='mt-8 text-center sm:col-span-2 xl:col-span-3'>
<Accent>Sorry, not found :(</Accent>
</h2>
);
}
20 changes: 20 additions & 0 deletions src/components/content/MDXComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Image from 'next/image';
import LiteYouTubeEmbed from 'react-lite-youtube-embed';

import SplitImage, { Split } from '@/components/content/SplitImage';
import CloudinaryImg from '@/components/images/CloudinaryImg';
import CustomLink from '@/components/links/CustomLink';
import TechIcons from '@/components/TechIcons';

const MDXComponents = {
a: CustomLink,
Image,
// code: CustomCode,
CloudinaryImg,
LiteYouTubeEmbed,
SplitImage,
Split,
TechIcons,
};

export default MDXComponents;
16 changes: 16 additions & 0 deletions src/components/content/ReloadDevTools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useRouter } from 'next/router';
import * as React from 'react';
import { HiRefresh } from 'react-icons/hi';

import ButtonLink from '@/components/links/ButtonLink';

export default function ReloadDevtool() {
const isProd = false;
const router = useRouter();

return !isProd ? (
<ButtonLink href={router.asPath} className='fixed bottom-4 left-4'>
<HiRefresh />
</ButtonLink>
) : null;
}
13 changes: 13 additions & 0 deletions src/components/content/SplitImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

export default function SplitImage({
children,
}: {
children: React.ReactNode;
}) {
return <div className='grid grid-cols-2 items-start gap-4'>{children}</div>;
}

export function Split({ children }: { children: React.ReactNode }) {
return <div className='!mb-0 flex flex-col space-y-4'>{children}</div>;
}
81 changes: 81 additions & 0 deletions src/components/content/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';

import TOCLink from '@/components/links/TOCLink';

export type HeadingScrollSpy = Array<{
id: string;
level: number;
text: string;
}>;

type TableOfContentsProps = {
toc?: HeadingScrollSpy;
activeSection: string | null;
minLevel: number;
};

export default function TableOfContents({
toc,
activeSection,
minLevel,
}: TableOfContentsProps) {
//#region //*=========== Scroll into view ===========
const lastPosition = React.useRef<number>(0);

React.useEffect(() => {
const container = document.getElementById('toc-container');
const activeLink = document.getElementById(`link-${activeSection}`);

if (container && activeLink) {
// Get container properties
const cTop = container.scrollTop;
const cBottom = cTop + container.clientHeight;

// Get activeLink properties
const lTop = activeLink.offsetTop - container.offsetTop;
const lBottom = lTop + activeLink.clientHeight;

// Check if in view
const isTotal = lTop >= cTop && lBottom <= cBottom;

const isScrollingUp = lastPosition.current > window.scrollY;
lastPosition.current = window.scrollY;

if (!isTotal) {
// Scroll by the whole clientHeight
const offset = 25;
const top = isScrollingUp
? lTop - container.clientHeight + offset
: lTop - offset;

container.scrollTo({ top, behavior: 'smooth' });
}
}
}, [activeSection]);
//#endregion //*======== Scroll into view ===========

return (
<div
id='toc-container'
className='hidden max-h-[calc(100vh-9rem-113px)] overflow-auto pb-4 lg:block'
>
<h3 className='text-gray-900 dark:text-gray-100 md:text-xl'>
Table of Contents
</h3>
<div className='mt-4 flex flex-col space-y-2 text-sm'>
{toc
? toc.map(({ id, level, text }) => (
<TOCLink
id={id}
key={id}
activeSection={activeSection}
level={level}
minLevel={minLevel}
text={text}
/>
))
: null}
</div>
</div>
);
}
Loading

0 comments on commit 9f1f1f1

Please sign in to comment.