Skip to content

Commit b6b272d

Browse files
committed
feat: add blog detail
1 parent 6c67cd8 commit b6b272d

File tree

13 files changed

+683
-47
lines changed

13 files changed

+683
-47
lines changed

next.config.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,6 @@ module.exports = {
1111
domains: ['res.cloudinary.com'],
1212
},
1313

14-
// SVGR
15-
webpack(config) {
16-
config.module.rules.push({
17-
test: /\.svg$/i,
18-
issuer: /\.[jt]sx?$/,
19-
use: [
20-
{
21-
loader: '@svgr/webpack',
22-
options: {
23-
typescript: true,
24-
icon: true,
25-
},
26-
},
27-
],
28-
});
29-
30-
return config;
31-
},
32-
3314
webpack: (config, { dev, isServer }) => {
3415
// Replace React with Preact only in client production build
3516
if (!dev && !isServer) {

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"prepare": "husky install"
1919
},
2020
"dependencies": {
21+
"@giscus/react": "^2.2.0",
2122
"ahooks": "^3.6.2",
2223
"axios": "^0.27.2",
2324
"cloudinary-build-url": "^0.2.4",
@@ -27,12 +28,15 @@
2728
"intercept-stdout": "^0.1.2",
2829
"lodash": "^4.17.21",
2930
"next": "^12.2.3",
31+
"next-themes": "^0.2.0",
3032
"react": "^18.2.0",
33+
"react-copy-to-clipboard": "^5.1.0",
3134
"react-dom": "^18.2.0",
3235
"react-hook-form": "^7.34.0",
3336
"react-icons": "^4.4.0",
3437
"react-image-lightbox": "^5.1.4",
3538
"react-intersection-observer": "^9.4.0",
39+
"react-lite-youtube-embed": "^2.3.1",
3640
"react-tippy": "^1.4.0",
3741
"recoil": "^0.7.4",
3842
"swr": "^1.3.0",

src/components/Seo.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const defaultMeta = {
1818
type SeoProps = {
1919
date?: string;
2020
templateTitle?: string;
21+
isBlog?: boolean;
22+
banner?: string;
2123
} & Partial<typeof defaultMeta>;
2224

2325
export default function Seo(props: SeoProps) {

src/components/TechIcons.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import clsx from 'clsx';
2+
import * as React from 'react';
3+
import { IoLogoVercel } from 'react-icons/io5';
4+
import {
5+
SiFirebase,
6+
SiGit,
7+
SiGoogleanalytics,
8+
SiJavascript,
9+
SiMarkdown,
10+
SiMongodb,
11+
SiNextdotjs,
12+
SiNodedotjs,
13+
SiNotion,
14+
SiPrettier,
15+
SiReact,
16+
SiRedux,
17+
SiSass,
18+
SiTailwindcss,
19+
SiTypescript,
20+
} from 'react-icons/si';
21+
22+
import Tooltip from '@/components/Tooltip';
23+
24+
export type TechListType = keyof typeof techList;
25+
26+
export type TechIconsProps = {
27+
techs: Array<TechListType>;
28+
} & React.ComponentPropsWithoutRef<'ul'>;
29+
30+
export default function TechIcons({ className, techs }: TechIconsProps) {
31+
return (
32+
<ul className={clsx(className, 'flex gap-2')}>
33+
{techs.map((tech) => {
34+
if (!techList[tech]) return;
35+
36+
const current = techList[tech];
37+
38+
return (
39+
<Tooltip key={current.name} content={<p>{current.name}</p>}>
40+
<li className='text-xl text-gray-700 dark:text-gray-200'>
41+
<current.icon />
42+
</li>
43+
</Tooltip>
44+
);
45+
})}
46+
</ul>
47+
);
48+
}
49+
50+
const techList = {
51+
react: {
52+
icon: SiReact,
53+
name: 'React',
54+
},
55+
nextjs: {
56+
icon: SiNextdotjs,
57+
name: 'Next.js',
58+
},
59+
tailwindcss: {
60+
icon: SiTailwindcss,
61+
name: 'Tailwind CSS',
62+
},
63+
scss: {
64+
icon: SiSass,
65+
name: 'SCSS',
66+
},
67+
javascript: {
68+
icon: SiJavascript,
69+
name: 'JavaScript',
70+
},
71+
typescript: {
72+
icon: SiTypescript,
73+
name: 'TypeScript',
74+
},
75+
nodejs: {
76+
icon: SiNodedotjs,
77+
name: 'Node.js',
78+
},
79+
firebase: {
80+
icon: SiFirebase,
81+
name: 'Firebase',
82+
},
83+
mongodb: {
84+
icon: SiMongodb,
85+
name: 'MongoDB',
86+
},
87+
swr: {
88+
icon: IoLogoVercel,
89+
name: 'SWR',
90+
},
91+
redux: {
92+
icon: SiRedux,
93+
name: 'Redux',
94+
},
95+
mdx: {
96+
icon: SiMarkdown,
97+
name: 'MDX',
98+
},
99+
prettier: {
100+
icon: SiPrettier,
101+
name: 'Prettier',
102+
},
103+
analytics: {
104+
icon: SiGoogleanalytics,
105+
name: 'Google Analytics',
106+
},
107+
git: {
108+
icon: SiGit,
109+
name: 'Git',
110+
},
111+
notion: {
112+
icon: SiNotion,
113+
name: 'Notion API',
114+
},
115+
};

src/components/content/MDXComponents.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import Quiz from '@/components/content/blog/Quiz';
55
import GithubCard from '@/components/content/card/GithubCard';
66
import CustomCode, { Pre } from '@/components/content/CustomCode';
77
import SplitImage, { Split } from '@/components/content/SplitImage';
8-
import TweetCard from '@/components/content/TweetCard';
98
import CloudinaryImg from '@/components/images/CloudinaryImg';
109
import CustomLink from '@/components/links/CustomLink';
1110
import TechIcons from '@/components/TechIcons';
@@ -20,7 +19,6 @@ const MDXComponents = {
2019
SplitImage,
2120
Split,
2221
TechIcons,
23-
TweetCard,
2422
GithubCard,
2523
Quiz,
2624
};

src/components/content/TweetCard.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/components/links/TOCLink.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import clsx from 'clsx';
2+
import * as React from 'react';
3+
4+
import UnstyledLink from '@/components/links/UnstyledLink';
5+
6+
type TOCLinkProps = {
7+
id: string;
8+
level: number;
9+
minLevel: number;
10+
text: string;
11+
activeSection: string | null;
12+
};
13+
14+
export default function TOCLink({
15+
id,
16+
level,
17+
minLevel,
18+
text,
19+
activeSection,
20+
}: TOCLinkProps) {
21+
return (
22+
<UnstyledLink
23+
href={`#${id}`}
24+
id={`link-${id}`}
25+
className={clsx(
26+
'font-medium hover:text-gray-700 focus:outline-none dark:hover:text-gray-200',
27+
'focus-visible:text-gray-700 dark:focus-visible:text-gray-200',
28+
activeSection === id
29+
? 'text-gray-900 dark:text-gray-100'
30+
: 'text-gray-400 dark:text-gray-500'
31+
)}
32+
style={{ marginLeft: (level - minLevel) * 16 }}
33+
>
34+
{text}
35+
</UnstyledLink>
36+
);
37+
}

src/lib/fauna.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import faunadb, { query as q } from 'faunadb';
2+
3+
import { AllContentRes, ContentMetaRes } from '@/types/fauna';
4+
5+
const faunaClient = new faunadb.Client({
6+
secret: process.env.FAUNA_SECRET as string,
7+
});
8+
9+
/**
10+
* Get all content meta from the database
11+
*/
12+
export const getAllContent = async () => {
13+
const { data } = await faunaClient.query<AllContentRes>(
14+
q.Map(
15+
q.Paginate(q.Documents(q.Collection('contents'))),
16+
q.Lambda((x) => q.Get(x))
17+
)
18+
);
19+
20+
return data.map((x) => x.data);
21+
};
22+
23+
/**
24+
* Get single content meta from the database by slug
25+
*/
26+
export const getContentMeta = async (slug: string) => {
27+
const data = await faunaClient.query<ContentMetaRes>(
28+
q.Let(
29+
{
30+
contentRef: q.Match(q.Index('get_contents_by_slug'), slug),
31+
contentExists: q.Exists(q.Var('contentRef')),
32+
},
33+
q.If(q.Var('contentExists'), q.Get(q.Var('contentRef')), null)
34+
)
35+
);
36+
37+
return data.data;
38+
};
39+
40+
/**
41+
* Create or add view count to the slug
42+
*/
43+
export const upsertContentMeta = async (slug: string) => {
44+
const data = await faunaClient.query<ContentMetaRes>(
45+
q.Let(
46+
{
47+
match: q.Match(q.Index('get_contents_by_slug'), slug),
48+
contentExists: q.Exists(q.Var('match')),
49+
},
50+
q.If(
51+
q.Var('contentExists'),
52+
q.Update(q.Select('ref', q.Get(q.Var('match'))), {
53+
data: {
54+
views: q.Add(q.Select(['data', 'views'], q.Get(q.Var('match'))), 1),
55+
},
56+
}),
57+
q.Create(q.Collection('contents'), {
58+
data: { slug: slug, views: 1, likes: 1, likesByUser: {} },
59+
})
60+
)
61+
)
62+
);
63+
64+
return data.data;
65+
};
66+
67+
/**
68+
* Create or add like count to the slug
69+
*/
70+
export const upsertLike = async (slug: string, sessionId: string) => {
71+
const data = await faunaClient.query<ContentMetaRes>(
72+
q.Let(
73+
{
74+
match: q.Match(q.Index('get_contents_by_slug'), slug),
75+
userLike: q.Select(
76+
['data', 'likesByUser', sessionId],
77+
q.Get(q.Var('match')),
78+
false
79+
),
80+
},
81+
82+
q.If(
83+
// If userLike is found
84+
q.IsInteger(q.Var('userLike')),
85+
q.If(
86+
// First check if we can add more
87+
q.GTE(q.Var('userLike'), 5),
88+
q.Abort('Max 5'),
89+
// Then Update the likes and update the likesByUser by sessionId
90+
q.Update(q.Select('ref', q.Get(q.Var('match'))), {
91+
data: {
92+
likes: q.Add(
93+
q.Select(['data', 'likes'], q.Get(q.Var('match'))),
94+
1
95+
),
96+
likesByUser: {
97+
[`${sessionId}`]: q.Add(q.Var('userLike'), 1),
98+
},
99+
},
100+
})
101+
),
102+
// Else, Update the likes and create new key with sessionId
103+
q.Update(q.Select('ref', q.Get(q.Var('match'))), {
104+
data: {
105+
likes: q.Add(q.Select(['data', 'likes'], q.Get(q.Var('match'))), 1),
106+
likesByUser: {
107+
[`${sessionId}`]: 1,
108+
},
109+
},
110+
})
111+
)
112+
)
113+
);
114+
115+
return data.data;
116+
};

src/lib/mdx.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export async function getFiles(type: ContentType) {
2020
}
2121

2222
export async function getFileBySlug(type: ContentType, slug: string) {
23-
const source = slug
23+
const mdxSource = slug
2424
? readFileSync(
2525
join(process.cwd(), 'src', 'contents', type, `${slug}.mdx`),
2626
'utf8'
@@ -30,8 +30,9 @@ export async function getFileBySlug(type: ContentType, slug: string) {
3030
'utf8'
3131
);
3232

33-
const { code, frontmatter } = await bundleMDX(source, {
34-
xdmOptions(options) {
33+
const { code, frontmatter } = await bundleMDX({
34+
source: mdxSource,
35+
mdxOptions(options, _) {
3536
options.remarkPlugins = [...(options?.remarkPlugins ?? []), remarkGfm];
3637
options.rehypePlugins = [
3738
...(options?.rehypePlugins ?? []),
@@ -53,8 +54,8 @@ export async function getFileBySlug(type: ContentType, slug: string) {
5354
return {
5455
code,
5556
frontmatter: {
56-
wordCount: source.split(/\s+/gu).length,
57-
readingTime: readingTime(source),
57+
wordCount: mdxSource.split(/\s+/gu).length,
58+
readingTime: readingTime(mdxSource),
5859
slug: slug || null,
5960
...frontmatter,
6061
},

0 commit comments

Comments
 (0)