Portfolio & Blog

React, Next.js, Markdown, Sass
The links to the GitHub project and website refer to the open-source and initial version of this project. The upgraded version is the very website you're currently navigating.

Description

Personal portfolio and blog website.

  • Developed with Next.js using Static Site Generation.
  • Dynamic pages for projects and blog posts.
  • Blog posts and project details written in markdown and rendered with react-markdown.
  • Framer motion, AOS and Swiper for the animations.


Project Diagram



Pages

index

Gets static props for featuredPosts and featuredProjects and holds components of the main page.

View code
1export default function Home(props) {
2  return (
3    <>
4      <Head>
5        // <title>
6        // <meta/>
7      </Head>
8      <Hero />
9      <FeaturedProjects featuredProjects={props.featuredProjects} />
10      <FeaturedPosts posts={props.posts} />
11      <About />
12    </>
13  );
14}
15
16export const getStaticProps = () => {
17  const featuredPosts = getFeaturedPosts();
18  const featuredProjects = getFeaturedProjects();
19
20  return {
21    props: {
22      posts: featuredPosts,
23      featuredProjects: featuredProjects,
24    },
25  };
26};
27

_app

Encapsulates the whole app making components like Navbar and Footer available on all pages. Sets a default theme and takes in a theme change from the Navbar component.

View code
1function MyApp({ Component, pageProps }) {
2  const [theme, setTheme] = useState("dark");
3  return (
4    <>
5      <div className="app" data-theme={theme}>
6        <Navbar theme={setTheme}>
7          <Head>
8            <meta
9              name="viewport"
10              content="width=device-width, initial-scale=1"
11            />
12            <link rel="shortcut icon" href="/favicon.ico" />
13          </Head>
14          <Component {...pageProps} currentTheme={theme} />
15          <Footer />
16        </Navbar>
17      </div>
18    </>
19  );
20}
21

_document

Custom 'Document' adds fonts and optimizes loading for all pages. The Head component used in '_document' is not the same as 'next/head'.

View code
1import Document, { Html, Head, Main, NextScript } from "next/document";
2
3class MyDocument extends Document {
4  render() {
5    return (
6      <Html lang="en">
7        <Head>
8          <link rel="preconnect" href="https://fonts.googleapis.com" />
9          <link
10            rel="preconnect"
11            href="https://fonts.gstatic.com"
12            crossOrigin="anonymous"
13          />
14          <link
15            href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Poppins&display=swap"
16            rel="stylesheet"
17          />
18
19          <link
20            rel="stylesheet"
21            href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
22            integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g=="
23            crossOrigin="anonymous"
24            referrerPolicy="no-referrer"
25          />
26        </Head>
27        <body>
28          <Main />
29          <NextScript />
30        </body>
31      </Html>
32    );
33  }
34}
35
36export default MyDocument;
37

\projects\index

getStaticProps for all projects and sends props to AllProjects component.

View code
1const Projects = (props) => {
2  const { projects } = props;
3
4  return (
5    <>
6      // <Head>
7      <AllProjects projects={projects} />
8    </>
9  );
10};
11export default Projects;
12
13export const getStaticProps = (context) => {
14  const allProjects = getAllProjects();
15
16  return {
17    props: {
18      projects: allProjects,
19    },
20  };
21};
22

\projects\[slug]

getStaticProps & getStaticPaths for all dynamic pages and sends props to ProjectContent component.

getProjectsFiles() gets all markdown files in the data directory.

'const slugs' maps through all markdown files and removes the '.md' extension and uses the file name as the slug.

View code
1const ProjectDetailPage = (props) => {
2  const { project, currentTheme } = props;
3
4  return (
5    <>
6      // <Head>
7      <ProjectContent project={project} currentTheme={currentTheme} />
8    </>
9  );
10};
11
12export const getStaticProps = (context) => {
13  const { params } = context;
14  const { slug } = params;
15  const projectData = getProjectData(slug);
16
17  return {
18    props: {
19      project: projectData,
20    },
21    revalidate: 600,
22  };
23};
24
25export const getStaticPaths = () => {
26  const projectsFilenames = getProjectsFiles();
27  const slugs = projectsFilenames.map((fileName) =>
28    fileName.replace(/\.md$/, '')
29  );
30
31  return {
32    paths: slugs.map((slug) => ({ params: { slug: slug } })),
33    fallback: false,
34  };
35};
36
37export default ProjectDetailPage;
38

Components

ProjectContent

ProjectContent explanation...

Renders the content you are currently reading form the markdown file and sets theme for code snippets with 'const customRenderers'

View code
1import ReactMarkdown from "react-markdown";
2import rehypeRaw from "rehype-raw";
3import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4import {
5  atomDark,
6  solarizedlight,
7} from "react-syntax-highlighter/dist/cjs/styles/prism";
8
9import Image from "next/image";
10import classes from "./projectContent.module.scss";
11import Link from "next/link";
12
13import { motion } from "framer-motion";
14import { Swiper, SwiperSlide } from "swiper/react";
15import { Navigation } from "swiper";
16import "swiper/css";
17import "swiper/css/pagination";
18import "swiper/css/navigation";
19
20const ProjectContent = (props) => {
21  const { project, currentTheme } = props;
22  const content = project.content;
23
24  const customRenderers = {
25    code(code) {
26      const { className, children } = code;
27      const language = className.split("-")[1]; // className is something like language-js => We need the "js" part here
28
29      return (
30        <>
31          {currentTheme === "dark" ? (
32            <SyntaxHighlighter
33              showLineNumbers
34              language={language}
35              style={atomDark}
36              // eslint-disable-next-line react/no-children-prop
37              children={children}
38            />
39          ) : (
40            <SyntaxHighlighter
41              showLineNumbers
42              language={language}
43              style={solarizedlight}
44              // eslint-disable-next-line react/no-children-prop
45              children={children}
46            />
47          )}
48        </>
49      );
50    },
51  };
52
53  return (
54    <div className={classes.projectDetail}>
55      <div className="container section mvh-100 projectDetail">
56        <Link href="/projects/">
57          <motion.button
58            whileHover={{ scale: 1.1 }}
59            whileTap={{ scale: 0.9 }}
60            className="btn btn-filled"
61          >
62            View All Projects
63          </motion.button>
64        </Link>
65
66        <div className={classes.card}>
67          <div className={classes.projectLinks}>
68            {project.githubLink && (
69              <a href={project.githubLink} target="_blank" rel="noreferrer">
70                <i className="fab fa-github"></i>
71                Github
72              </a>
73            )}
74            {project.liveLink && (
75              <a href={project.liveLink} target="_blank" rel="noreferrer">
76                <i className="fas fa-link"></i>
77                Website
78              </a>
79            )}
80          </div>
81
82          <h1>{project.title}</h1>
83          <small>
84            {Array.isArray(project.tech)
85              ? project.tech.join(", ")
86              : project.tech}
87          </small>
88
89          {project.image && (
90            <div className={classes.projectImage}>
91              <Image
92                src={`../../images/projects/${project.image}`}
93                width={500}
94                height={360}
95                alt=""
96              />
97            </div>
98          )}
99
100          <ReactMarkdown
101            components={customRenderers}
102            rehypePlugins={[rehypeRaw]}
103          >
104            {content}
105          </ReactMarkdown>
106
107          {project.screenshots && (
108            <div className="mb-50">
109              <h2>Screenshots</h2>
110              <Swiper
111                rewind={true}
112                grabCursor={true}
113                modules={[Navigation]}
114                navigation={true}
115                className="mySwiper"
116              >
117                {project.screenshots.map((screenshot, index) => (
118                  <SwiperSlide key={index}>
119                    <Image
120                      src={`../../images/projects/${project.slug}/${screenshot.screenshot}`}
121                      width={1000}
122                      height={700}
123                      alt={screenshot.description}
124                    />
125                    <div className={classes.description}>
126                      {index + 1}. {screenshot.description}
127                    </div>
128                  </SwiperSlide>
129                ))}
130              </Swiper>
131            </div>
132          )}
133        </div>
134      </div>
135    </div>
136  );
137};
138
139export default ProjectContent;
140