diff --git a/commitlint.config.js b/commitlint.config.js
index 3bb2de2d..1fcb76ab 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -21,6 +21,7 @@ const CommitLintConfiguration = {
"projects",
"seo",
"services",
+ "snippets",
"static",
"theme",
"utils",
diff --git a/content/snippets/custom-scroll-bar.md b/content/snippets/custom-scroll-bar.md
new file mode 100644
index 00000000..410595f2
--- /dev/null
+++ b/content/snippets/custom-scroll-bar.md
@@ -0,0 +1,40 @@
+---
+title: Custom Scrollbar
+description: define your own custom scroll bar
+published: false
+date: 2022-07-22
+stacks:
+ - css
+ - chakra-ui
+---
+
+## CSS
+
+```css
+::-webkit-scrollbar {
+ width: 0.75rem;
+ height: 0.75rem;
+ background-color: blue;
+}
+
+::-webkit-scrollbar-thumb {
+ border-radius: 20px;
+ background-color: gray;
+}
+
+/** firefox **/
+html {
+ scrollbar-width: thin;
+ scrollbar-color: blue;
+}
+```
+
+## References
+
+- MDN
+ - [https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar#browser_compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar#browser_compatibility)
+ - [https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scrollbars#browser_compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scrollbars#browser_compatibility)
+- W3Schools: [https://www.w3schools.com/howto/howto_css_custom_scrollbar.asp](https://www.w3schools.com/howto/howto_css_custom_scrollbar.asp)
+- [sznm.dev](http://sznm.dev) - chakra-ui implementation
+ - [https://github.com/sozonome/sznm.dev/commit/f967221e40c7d680eb25ca4944ede4f5def2b628](https://github.com/sozonome/sznm.dev/commit/f967221e40c7d680eb25ca4944ede4f5def2b628)
+ - [https://github.com/sozonome/sznm.dev/commit/76c3ce6895b6de5b0a0d4195378cf68cde269054](https://github.com/sozonome/sznm.dev/commit/76c3ce6895b6de5b0a0d4195378cf68cde269054)
diff --git a/content/snippets/overflow-scroll-without-scrollbar.md b/content/snippets/overflow-scroll-without-scrollbar.md
new file mode 100644
index 00000000..8de20c6c
--- /dev/null
+++ b/content/snippets/overflow-scroll-without-scrollbar.md
@@ -0,0 +1,34 @@
+---
+title: Overflow Scroll without Scrollbar
+description: for sleek overflow scroll in mobile viewport
+published: false
+date: 2022-07-22
+stacks:
+ - css
+ - chakra-ui
+---
+
+## CSS
+
+```css
+.some-component {
+ overflow-x: scroll; /* or overflow-y */
+}
+
+.some-component::-webkit-scrollbar {
+ display: none;
+}
+```
+
+## Chakra-UI
+
+```jsx
+
+ ...some children
+
+```
+
+## References
+
+- [https://stackoverflow.com/questions/65042380/how-to-add-webkit-scrollbar-pseudo-element-in-chakra-ui-element-react](https://stackoverflow.com/questions/65042380/how-to-add-webkit-scrollbar-pseudo-element-in-chakra-ui-element-react)
+- [https://react.geist-ui.dev/en-us/components/tabs](https://react.geist-ui.dev/en-us/components/tabs)
diff --git a/contentlayer.config.ts b/contentlayer.config.ts
index 8809fa5f..fe3e1e33 100644
--- a/contentlayer.config.ts
+++ b/contentlayer.config.ts
@@ -77,9 +77,29 @@ const Project = defineDocumentType(() => ({
},
}));
+const Snippet = defineDocumentType(() => ({
+ name: "Snippet",
+ filePathPattern: "snippets/*.md",
+ fields: {
+ title: { type: "string", required: true },
+ description: { type: "string", required: true },
+ published: { type: "boolean" },
+ date: { type: "string" },
+ stacks: { type: "list", of: { type: "string" } },
+ },
+ computedFields: {
+ id: {
+ type: "string",
+ resolve: (snippet) =>
+ // eslint-disable-next-line no-underscore-dangle
+ snippet._raw.sourceFileName.replace(/\.md$|\.mdx$/, ""),
+ },
+ },
+}));
+
const contentLayerConfig = makeSource({
contentDirPath: "content",
- documentTypes: [Blog, Project],
+ documentTypes: [Blog, Project, Snippet],
markdown: {
remarkPlugins: [remarkHtml],
rehypePlugins: [rehypeRaw],
diff --git a/src/lib/components/blog/renderers.tsx b/src/lib/components/blog/renderers.tsx
index 975be1c6..433405db 100644
--- a/src/lib/components/blog/renderers.tsx
+++ b/src/lib/components/blog/renderers.tsx
@@ -55,5 +55,3 @@ export const renderers: Options["components"] = {
h5: ({ children }) => {String(children)},
h6: ({ children }) => {String(children)},
};
-
-export default renderers;
diff --git a/src/lib/components/snippets/SnippetCard.tsx b/src/lib/components/snippets/SnippetCard.tsx
new file mode 100644
index 00000000..175739f3
--- /dev/null
+++ b/src/lib/components/snippets/SnippetCard.tsx
@@ -0,0 +1,56 @@
+import { Flex, Grid, Heading, Text } from "@chakra-ui/react";
+import type { Snippet } from "contentlayer/generated";
+import Link from "next/link";
+
+import { trackEventToUmami } from "lib/utils/trackEvent";
+
+type SnippetCardProps = {
+ data: Snippet;
+};
+
+const SnippetCard = ({ data }: SnippetCardProps) => {
+ const handleClickSnippet = () => {
+ trackEventToUmami({
+ eventValue: `Snippet: ${data.title}`,
+ eventType: "navigate",
+ });
+ };
+
+ return (
+
+
+
+ {data.title}
+ {data.description}
+
+
+
+ {data.stacks?.map((stack) => (
+
+ {stack}
+
+ ))}
+
+
+
+ );
+};
+
+export default SnippetCard;
diff --git a/src/lib/components/snippets/detail/Head.tsx b/src/lib/components/snippets/detail/Head.tsx
new file mode 100644
index 00000000..60f4a965
--- /dev/null
+++ b/src/lib/components/snippets/detail/Head.tsx
@@ -0,0 +1,19 @@
+import { Grid, Heading, Text } from "@chakra-ui/react";
+import type { Snippet } from "contentlayer/generated";
+
+type SnippetDetailHeadProps = {
+ data: Snippet;
+};
+
+const SnippetDetailHead = ({ data }: SnippetDetailHeadProps) => {
+ return (
+
+
+ {data.title}
+
+ {data.description}
+
+ );
+};
+
+export default SnippetDetailHead;
diff --git a/src/lib/components/snippets/detail/Meta.tsx b/src/lib/components/snippets/detail/Meta.tsx
new file mode 100644
index 00000000..961972de
--- /dev/null
+++ b/src/lib/components/snippets/detail/Meta.tsx
@@ -0,0 +1,35 @@
+import type { Snippet } from "contentlayer/generated";
+import { NextSeo } from "next-seo";
+
+import { baseUrl } from "lib/constants/baseUrl";
+import { sznmOgImage } from "lib/utils/sznmOgImage";
+
+type SnippetDetailMetaProps = {
+ data: Snippet;
+};
+
+const SnippetDetailMeta = ({ data }: SnippetDetailMetaProps) => {
+ const ogImage = sznmOgImage({
+ heading: data.title,
+ text: "Snippets | https://sznm.dev",
+ });
+ const pageUrl = `${baseUrl}/snippets/${data.id}`;
+
+ return (
+
+ );
+};
+
+export default SnippetDetailMeta;
diff --git a/src/lib/layout/Navigation.tsx b/src/lib/layout/Navigation.tsx
index e76d976b..62f9cc35 100644
--- a/src/lib/layout/Navigation.tsx
+++ b/src/lib/layout/Navigation.tsx
@@ -1,7 +1,7 @@
import { IconButton } from "@chakra-ui/react";
import Link from "next/link";
import type { IconType } from "react-icons";
-import { FaFeatherAlt, FaHome, FaRocket, FaUser } from "react-icons/fa";
+import { FaCode, FaFeatherAlt, FaHome, FaRocket, FaUser } from "react-icons/fa";
import { trackEventToUmami } from "lib/utils/trackEvent";
@@ -26,7 +26,7 @@ const NavItem = ({ href, label, icon }: NavItemProps) => {
aria-label={label}
variant="ghost"
flexBasis="25%"
- fontSize={["2xl", "md"]}
+ fontSize={["xl", "md"]}
padding={0}
onClick={handleClickNavigation}
>
@@ -52,6 +52,11 @@ const navigations: NavItemProps[] = [
label: "Blog",
icon: FaFeatherAlt,
},
+ {
+ href: "/snippets",
+ label: "Snippets",
+ icon: FaCode,
+ },
{
href: "/about",
label: "About",
diff --git a/src/lib/pages/snippets/detail/Snippet.module.scss b/src/lib/pages/snippets/detail/Snippet.module.scss
new file mode 100644
index 00000000..b4d993ea
--- /dev/null
+++ b/src/lib/pages/snippets/detail/Snippet.module.scss
@@ -0,0 +1,26 @@
+.content {
+ p {
+ margin: 0.5rem 0 2rem;
+ }
+ a {
+ font-weight: 550;
+ text-decoration: underline;
+ }
+ img {
+ border-radius: 1rem;
+ margin: 1rem 0;
+ }
+ ul,
+ ol {
+ margin: 0 1.5rem;
+ li {
+ margin: 0.6rem 0;
+ }
+ }
+ code {
+ background-color: rgb(200, 200, 200);
+ border-radius: 0.25rem;
+ padding: 0 0.25rem;
+ color: black;
+ }
+}
diff --git a/src/lib/pages/snippets/detail/index.tsx b/src/lib/pages/snippets/detail/index.tsx
new file mode 100644
index 00000000..9e76f197
--- /dev/null
+++ b/src/lib/pages/snippets/detail/index.tsx
@@ -0,0 +1,46 @@
+import { Box, Spacer, useColorModeValue } from "@chakra-ui/react";
+import type { GiscusProps } from "@giscus/react";
+import Giscus from "@giscus/react";
+import ReactMarkdown from "react-markdown";
+import rehypeRaw from "rehype-raw";
+
+import { renderers } from "lib/components/blog/renderers";
+import SnippetDetailHead from "lib/components/snippets/detail/Head";
+import SnippetDetailMeta from "lib/components/snippets/detail/Meta";
+
+import styles from "./Snippet.module.scss";
+import type { SnippetDetailProps } from "./types";
+
+const SnippetDetail = ({ data }: SnippetDetailProps) => {
+ const giscusTheme: GiscusProps["theme"] = useColorModeValue("light", "dark");
+
+ return (
+
+
+
+
+
+ {data.body.raw}
+
+
+
+
+
+
+ );
+};
+
+export default SnippetDetail;
diff --git a/src/lib/pages/snippets/detail/loader.ts b/src/lib/pages/snippets/detail/loader.ts
new file mode 100644
index 00000000..5c05f272
--- /dev/null
+++ b/src/lib/pages/snippets/detail/loader.ts
@@ -0,0 +1,30 @@
+import type { Snippet } from "contentlayer/generated";
+import { allSnippets } from "contentlayer/generated";
+import type { GetStaticProps } from "next";
+
+import type { SnippetDetailParams, SnippetDetailProps } from "./types";
+
+export const getStaticPaths = async () => {
+ const paths = allSnippets.map((project) => ({
+ params: {
+ id: project.id,
+ },
+ }));
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export const getStaticProps: GetStaticProps<
+ SnippetDetailProps,
+ SnippetDetailParams
+> = async ({ params }) => {
+ const data = allSnippets.find(
+ ({ id }) => id === (params?.id as string)
+ ) as Snippet;
+
+ return {
+ props: { data },
+ };
+};
diff --git a/src/lib/pages/snippets/detail/types.ts b/src/lib/pages/snippets/detail/types.ts
new file mode 100644
index 00000000..7bc8d283
--- /dev/null
+++ b/src/lib/pages/snippets/detail/types.ts
@@ -0,0 +1,9 @@
+import type { Snippet } from "contentlayer/generated";
+
+export type SnippetDetailParams = {
+ id: string;
+};
+
+export type SnippetDetailProps = {
+ data: Snippet;
+};
diff --git a/src/lib/pages/snippets/list/index.tsx b/src/lib/pages/snippets/list/index.tsx
new file mode 100644
index 00000000..1359e9db
--- /dev/null
+++ b/src/lib/pages/snippets/list/index.tsx
@@ -0,0 +1,38 @@
+import { Grid, Heading, Text } from "@chakra-ui/react";
+
+import MotionBox from "lib/components/motion/MotionBox";
+import MotionGrid from "lib/components/motion/MotionGrid";
+import SnippetCard from "lib/components/snippets/SnippetCard";
+import {
+ childAnimationProps,
+ staggerAnimationProps,
+} from "lib/constants/animation";
+
+import type { SnippetListProps } from "./types";
+
+const SnippetList = ({ snippets }: SnippetListProps) => {
+ return (
+
+
+ Snippets
+
+ A collection of my personal snippets I use throughout my projects
+
+
+
+
+ {snippets.map((item) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+export default SnippetList;
diff --git a/src/lib/pages/snippets/list/loader.ts b/src/lib/pages/snippets/list/loader.ts
new file mode 100644
index 00000000..2aa77499
--- /dev/null
+++ b/src/lib/pages/snippets/list/loader.ts
@@ -0,0 +1,14 @@
+import { allSnippets } from "contentlayer/generated";
+import type { GetStaticProps } from "next";
+
+import type { SnippetListProps } from "./types";
+
+export const getStaticProps: GetStaticProps = () => {
+ const snippets = allSnippets;
+
+ return {
+ props: {
+ snippets,
+ },
+ };
+};
diff --git a/src/lib/pages/snippets/list/types.ts b/src/lib/pages/snippets/list/types.ts
new file mode 100644
index 00000000..710a8380
--- /dev/null
+++ b/src/lib/pages/snippets/list/types.ts
@@ -0,0 +1,5 @@
+import type { Snippet } from "contentlayer/generated";
+
+export type SnippetListProps = {
+ snippets: Array;
+};
diff --git a/src/pages/snippets/[id].ts b/src/pages/snippets/[id].ts
new file mode 100644
index 00000000..deb7d7c8
--- /dev/null
+++ b/src/pages/snippets/[id].ts
@@ -0,0 +1,7 @@
+import SnippetDetail from "lib/pages/snippets/detail";
+
+export {
+ getStaticPaths,
+ getStaticProps,
+} from "lib/pages/snippets/detail/loader";
+export default SnippetDetail;
diff --git a/src/pages/snippets/index.ts b/src/pages/snippets/index.ts
new file mode 100644
index 00000000..ed6e4a0c
--- /dev/null
+++ b/src/pages/snippets/index.ts
@@ -0,0 +1,4 @@
+import SnippetList from "lib/pages/snippets/list";
+
+export { getStaticProps } from "lib/pages/snippets/list/loader";
+export default SnippetList;