Portfolio & Blog
React, Next.js, Markdown, SassThe 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