<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Louis Gustavo on Medium]]></title>
        <description><![CDATA[Stories by Louis Gustavo on Medium]]></description>
        <link>https://medium.com/@leejhlouis?source=rss-4be39b68de2f------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*W3zedqssCyfDj3cF.jpg</url>
            <title>Stories by Louis Gustavo on Medium</title>
            <link>https://medium.com/@leejhlouis?source=rss-4be39b68de2f------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 15 Apr 2026 18:38:32 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@leejhlouis/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How I built my portfolio site with React, TypeScript, and Tailwind CSS?]]></title>
            <link>https://medium.com/@leejhlouis/how-i-build-my-portfolio-site-with-react-typescript-and-tailwind-css-479778cf2960?source=rss-4be39b68de2f------2</link>
            <guid isPermaLink="false">https://medium.com/p/479778cf2960</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[portfolio]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[Louis Gustavo]]></dc:creator>
            <pubDate>Thu, 30 May 2024 10:39:06 GMT</pubDate>
            <atom:updated>2025-02-14T15:17:22.114Z</atom:updated>
            <content:encoded><![CDATA[<p>As a recent computer science graduate seeking opportunities, the necessity of a visually appealing portfolio website showcasing my accomplishments is important to provide hiring managers with a better idea of who I am.</p><p>In the past, I managed to build a <a href="https://leejhlouis.github.io/louisite-v1/">portfolio site</a> using vanilla HTML, CSS, and JavaScript.</p><p>It was a good first try though. However, it failed to highlight my featured projects, the most crucial part of the portfolio website. It is mainly due to other sections — to name a few, my experience, educational background, and skills — taking precedence over the projects section. In addition, I was personally not satisfied with the first iteration’s appearance.</p><p>This prompted me to develop the second iteration with an all-new and refreshed layout and appearance. Check it out below!</p><p><a href="https://louisite.com/">Louis Gustavo | Software Engineer</a></p><p>For the second iteration, I decided to build a multi-page site including the home page, about page, and the projects page.</p><p>There are many ways to build a portfolio site. I decided to build mine from scratch to challenge myself to learn React and TypeScript.</p><p>In this article, I will show you the tech stack and techniques used to build the site.</p><h3>Tech stack</h3><p>The website was initially bootstrapped with <a href="https://create-react-app.dev/">Create React App</a> (CRA) and built with React, TypeScript, and Tailwind CSS. It’s deployed with Netlify.</p><p>React was chosen due to its beautifully curated JavaScript library for UI components and TypeScript was chosen due to its type-checking feature. Meanwhile, Tailwind CSS was used as the CSS framework for its practical utility classes to build the interface. Furthermore, I use Markdown to manage the content of the <a href="https://louisite.com/about">About Me</a> page.</p><p>In 2023, I <a href="https://github.com/leejhlouis/louisite/pull/9">migrated</a> CRA to <a href="https://vitejs.dev/">Vite</a>, a simpler and faster build tool alternative to CRA.</p><h3>Techniques</h3><h4>Light and dark theme</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BJPyf6aPdzZhQ-7GUGBL0w.png" /><figcaption>Light mode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xtUs2Cf2OtJdWq95awGNBw.png" /><figcaption>Dark theme</figcaption></figure><p>I also created a dark mode for the site by utilizing Tailwind CSS’s <a href="https://tailwindcss.com/docs/dark-mode">dark mode variant</a>.</p><p>I set the property of darkMode as class in the Tailwind CSS config file. This is done to automatically enable a dark theme when the dark class is added on the HTML tag (&lt;html&gt;).</p><pre>// tailwind.config.ts<br>export default {<br>  content: [&#39;index.html&#39;, &#39;./src/**/*.{js,jsx,ts,tsx}&#39;],<br>  darkMode: &#39;class&#39;,<br>  ...<br>}</pre><p>If the theme is toggled to dark, the HTML tag will have the dark class.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/541/1*cY6466o_056cs-XXIN2U0g.png" /><figcaption>The `dark` class in the HTML tag (&lt;html&gt;)</figcaption></figure><p>On my App.tsx file, the site initially checks the users’ preferred color theme based on the browser settings and the last used theme stored on the browser’s local storage.</p><pre>// src/App.tsx<br>import { useEffect } from &#39;react&#39;<br>import checkDarkTheme from &#39;@/utils/checkDarkTheme.ts&#39;=<br><br>export default function App() {<br>  useEffect(() =&gt; {<br>    if (checkDarkTheme()) {<br>      document.documentElement.classList.add(&#39;dark&#39;)<br>      return<br>    }<br>    document.documentElement.classList.remove(&#39;dark&#39;)<br>  }, [])<br><br>  return (<br>    {/* App components */}<br>  )<br>}</pre><pre>// src/utils/checkDarkTheme.ts<br>export default function checkDarkTheme() {<br>  return (<br>    localStorage.theme === &#39;dark&#39; ||<br>    (!(&#39;theme&#39; in localStorage) &amp;&amp; window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches)<br>  )<br>}</pre><p>By default, this website uses the users’ preferred color theme, although the users can also manually override the theme.</p><pre>// src/components/common/ThemeSwitcher.tsx<br>import { lazy, useState } from &#39;react&#39;<br>import SunLineIcon from &#39;remixicon-react/SunLineIcon.js&#39;<br>import MoonLineIcon from &#39;remixicon-react/MoonLineIcon.js&#39;<br>import checkDarkTheme from &#39;@/utils/checkDarkTheme.ts&#39;<br><br>const IconButton = lazy(() =&gt; import(&#39;@/components/common/reusable/button/IconButton.tsx&#39;))<br><br>export default function ThemeSwitcher() {<br>  const [isDark, setDark] = useState(checkDarkTheme)<br><br>  const toggleDarkTheme = () =&gt; {<br>    document.documentElement.classList.toggle(&#39;dark&#39;)<br>    localStorage.theme = isDark ? &#39;light&#39; : &#39;dark&#39;<br>    setDark(!isDark)<br>  }<br><br>  return (<br>    &lt;IconButton<br>      className=&#39;duration-300&#39;<br>      icon={isDark ? &lt;MoonLineIcon size={20} /&gt; : &lt;SunLineIcon size={20} /&gt;}<br>      screenReaderText=&#39;Toggle theme&#39;<br>      onClick={toggleDarkTheme}<br>    /&gt;<br>  )<br>}</pre><p>When the theme is toggled, the dark class on the HTML tag will be toggled and the theme stored on the local storage will be changed. Then, the state isDark is also changed as it’s used to determine the icon inside the theme switcher button (I use remixicon-react library for the icons).</p><p>For the IconButton component, you may check the <a href="https://github.com/leejhlouis/louisite/blob/main/src/components/common/ThemeSwitcher.tsx">file</a>.</p><h4>Glassmorphism UI design</h4><p>Popularized by <a href="https://uxdesign.cc/glassmorphism-in-user-interfaces-1f39bb1308c9">Michal Malewicz</a>, glassmorphism is a new UI design trend where design elements have the effect of mimicking frosted glass.</p><p>On my website, I implemented a glassmorphism design on the navbar with Tailwind CSS’s <a href="https://tailwindcss.com/docs/backdrop-blur">backdrop blur</a> utility class. It’s used to make the frosted glass effect when the navbar is scrolled.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Iknw-91uwMnBWcZluO0bQA.png" /><figcaption>The frosted glass effect on the navbar when it’s scrolled</figcaption></figure><pre>&lt;nav className=&#39;fixed top-0 z-50 w-full backdrop-blur-xl&#39;&gt;<br>  &lt;div className=&#39;container flex flex-wrap items-center justify-between py-4 xl:max-w-screen-xl&#39;&gt;<br>    {/* Navbar content */}<br>  &lt;/div&gt;<br>&lt;/nav&gt;</pre><p>I also implemented a glassmorphism design on the project card. Here’s how I do it with Tailwind CSS classes.</p><pre>&lt;ul&gt;<br> &lt;li<br>    className={clsx(<br>      &#39;rounded-xl border border-slate-500/20 dark:border-slate-600/30&#39;,<br>      &#39;bg-slate-100/20 dark:bg-slate-600/20&#39;,<br>      &#39;hover:bg-slate-100/30 dark:hover:bg-slate-600/30&#39;<br>    )}<br>    &gt;<br>    {/* Card content */}<br>  &lt;/li&gt;<br>&lt;/ul&gt;</pre><p>As seen in the above snippet, I use the opacity modifier in the Tailwind classes. This is to make the cards more but not wholly transparent and have the frosted glass effects.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S5TPyH3m2V6wX7olfo0jmg.png" /><figcaption>Project list cards with frosted glass effects</figcaption></figure><h4>Rotating linear gradient highlight effect</h4><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*BJPyf6aPdzZhQ-7GUGBL0w.png" /><figcaption>Rotating linear gradient highlight effect as seen in the text “Computer Science” and “Software Engineering”</figcaption></figure><p>To make the title highlight more appealing, I added a rotating linear gradient effect on the title section. The gradient on the highlight text will rotate 15 degrees every 75 milliseconds.</p><pre>import { useEffect, useRef, useState } from &#39;react&#39;<br>import ComponentProps from &#39;@/types/components/ComponentProps&#39;<br>import clsx from &#39;clsx&#39;<br><br>export default function HighlightText({ children }: ComponentProps) {<br>  const ref = useRef&lt;HTMLLinkElement&gt;(null)<br>  const [degree, setDegree] = useState(0)<br><br>  useEffect(() =&gt; {<br>    const interval = setInterval(() =&gt; {<br>      setDegree((degree + 15) % 360)<br>      if (ref.current) {<br>        ref.current.style.backgroundImage = `linear-gradient(${degree}deg, var(--tw-gradient-stops))`<br>      }<br>    }, 75)<br>    return () =&gt; clearInterval(interval)<br>  })<br><br>  return (<br>    &lt;span<br>      ref={ref}<br>      className={clsx(<br>        &#39;from-fuchsia-700 to-indigo-700 bg-clip-text&#39;,<br>        &#39;dark:from-fuchsia-400 dark:to-blue-400&#39;,<br>        &#39;text-transparent transition&#39;<br>      )}<br>    &gt;<br>      {children}<br>    &lt;/span&gt;<br>  )</pre><p>I set the element’s backgroundImage property by using React’s <a href="https://react.dev/reference/react/useRef">useRef</a> hook. Then, I use JavaScript’s setInterval() method for updating the gradient degree every 75 ms.</p><h4>React Markdown</h4><p><a href="https://www.npmjs.com/package/react-markdown">React Markdown</a> is a library for rendering Markdown files. This library is useful for styling the rendered markdown, for instance, the headings, the inline links, the lists, and more. I use my reusable components for the site.</p><p>On my site, I use React Markdown for the <a href="https://louisite.netlify.app/about">About</a> page’s content management to avoid hard coding, making it easier to maintain.</p><p>Here’s how I do it:</p><pre>import { lazy } from &#39;react&#39;<br><br>const ReactMarkdown = lazy(() =&gt; import(&#39;react-markdown&#39;))<br>const Heading1 = lazy(() =&gt; import(&#39;@/components/common/reusable/heading/Heading1&#39;))<br>const Heading2 = lazy(() =&gt; import(&#39;@/components/common/reusable/heading/Heading2&#39;))<br>const Heading3 = lazy(() =&gt; import(&#39;@/components/common/reusable/heading/Heading3&#39;))<br>const Badge = lazy(() =&gt; import(&#39;@/components/common/reusable/Badge&#39;))<br>const InlineLink = lazy(() =&gt; import(&#39;@/components/common/reusable/InlineLink.tsx&#39;))<br><br>export default function About({ children }: ComponentProps) {<br>   return (<br>    &lt;ReactMarkdown<br>      components={{<br>        h1: Heading1,<br>        h2: Heading2,<br>        h3: Heading3,<br>        a: InlineLink,<br>        ul: ({ children }) =&gt;<br>          (&lt;ul className=&#39;mb-8 flex flex-wrap gap-2&#39;&gt;{children}&lt;/ul&gt;) as JSX.Element,<br>        li: ({ children }) =&gt; (&lt;Badge&gt;{children}&lt;/Badge&gt;) as JSX.Element<br>      }}<br>    &gt;<br>      {(localStorage.about as string) ?? children}<br>    &lt;/ReactMarkdown&gt;<br>  )<br>}</pre><p>This is the result of the rendered markdown.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v2hUEpJTg8GwQFlpvhFwGQ.png" /><figcaption>Markdown render result</figcaption></figure><h4>JavaScript array of objects for storing data</h4><p>In this iteration, I decided to use a JavaScript array of objects to store my projects’ data. This is a very useful approach to avoid hard coding and make the contents easier to manage.</p><p>It kind of looks like this.</p><pre>import GithubFillIcon from &#39;remixicon-react/GithubFillIcon.js&#39;<br>import ExternalLinkFillIcon from &#39;remixicon-react/ExternalLinkFillIcon.js&#39;<br>import getGitHubUrl from &#39;@/utils/getGitHubUrl&#39;<br>import LinkProps from &#39;@/types/LinkProps&#39;<br>import ProjectProps from &#39;@/types/components/ProjectProps&#39;<br>import constants from &#39;@/constants&#39;<br>import InlineLink from &#39;@/components/common/reusable/InlineLink&#39;<br><br>const github: LinkProps = {<br>  label: &#39;Source code&#39;,<br>  icon: &lt;GithubFillIcon size={24} /&gt;<br>}<br><br>const live: LinkProps = {<br>  label: &#39;Live&#39;,<br>  icon: &lt;ExternalLinkFillIcon size={24} /&gt;<br>}<br><br>const getLinks = (githubRepo: string, url?: string) =&gt; {<br>  const links = [{ ...github, url: getGitHubUrl(githubRepo) }]<br>  if (url) {<br>    links.push({ ...live, url })<br>  }<br>  return links<br>}<br><br>const filters = [<br>  &#39;React&#39;,<br>  &#39;Vue.js&#39;,<br>  &#39;Laravel&#39;,<br>  &#39;TypeScript&#39;,<br>  &#39;JavaScript&#39;,<br>  &#39;jQuery&#39;,<br>  &#39;Tailwind CSS&#39;,<br>  &#39;Bootstrap&#39;,<br>  &#39;HTML/CSS&#39;,<br>  &#39;PHP&#39;,<br>  &#39;Java&#39;,<br>  &#39;Python&#39;,<br>  &#39;ASP.NET&#39;,<br>  &#39;Android SDK&#39;,<br>  &#39;Firebase&#39;,<br>  &#39;Axios Mock&#39;<br>]<br><br>const projects: ProjectProps[] = [<br>  {<br>    id: &#39;louisite&#39;,<br>    featured: true,<br>    title: &#39;LOUISITE&#39;,<br>    description:<br>      &#39;My all-new personal website—this is the second and latest iteration—built with React and TypeScript.&#39;,<br>    techStacks: [&#39;React&#39;, &#39;TypeScript&#39;, &#39;Tailwind CSS&#39;],<br>    otherTechStacks: [&#39;HTML/CSS&#39;, &#39;JavaScript&#39;],<br>    category: &#39;Front-end development&#39;,<br>    links: getLinks(&#39;louisite&#39;, &#39;https://louisite.netfliy.app&#39;)<br>  },<br>  {<br>    id: &#39;vue-member-management&#39;,<br>    featured: true,<br>    title: &#39;Member Management App&#39;,<br>    description: (<br>      &lt;span&gt;<br>        A member management system app built with Vue.js. Built as a probation project during my<br>        internship at &lt;InlineLink href=&#39;https://blibli.com&#39;&gt;Blibli&lt;/InlineLink&gt;.<br>      &lt;/span&gt;<br>    ),<br>    techStacks: [&#39;Vue.js&#39;, &#39;Axios Mock&#39;],<br>    otherTechStacks: [&#39;HTML/CSS&#39;, &#39;JavaScript&#39;],<br>    category: &#39;Front-end development&#39;,<br>    links: getLinks(&#39;vue-member-management&#39;)<br>  },<br>]<br><br>export { filters, projects }</pre><h4>clsx</h4><p>Tailwind CSS classes are too long and it keeps making my code messy. To avoid that, I used the <a href="https://github.com/lukeed/clsx">clsx</a> library for grouping similar classes.</p><pre>&lt;button<br>  className={clsx(<br>    className,<br>    &#39;group/underline flex w-fit items-center space-x-1 transition duration-300 ease-in-out&#39;,<br>    {<br>      &#39;font-extrabold text-primary-dark dark:text-white&#39;: active,<br>      &#39;font-semibold&#39;: !active<br>    },<br>    {<br>      &#39;rounded-xl px-3 py-1&#39;: inverted,<br>      &#39;text-primary-dark dark:text-primary-light&#39;: inverted,<br>      &#39;hover:bg-primary-dark/5 dark:hover:bg-primary-light/5&#39;: inverted &amp;&amp; !active,<br>      &#39;hover:text-primary-dark dark:hover:text-primary-light&#39;: !inverted &amp;&amp; !active<br>    }<br>  )}<br>&gt;&lt;/button&gt;</pre><p>Additionally, clsx is useful for setting classes conditionally. For example, classes &#39;rounded-xl px-3 py-1&#39; will only be set if the value of the variable inverted is true.</p><h3>Deployment</h3><p>I considered Netlify for the deployment tools. It’s because Netlify is simple to use, especially for beginners like me. You just have to integrate Netlify with your GitHub account (make sure to push your portfolio to your GitHub) and select your portfolio repository to deploy it on Netlify.</p><p>For more detailed step-by-step instructions, you may follow this <a href="https://www.netlify.com/blog/2016/09/29/a-step-by-step-guide-deploying-on-netlify/">guide</a>.</p><h3>Resources</h3><p>Live demo: <a href="https://louisite.com">https://louisite.com</a><br>Source code: <a href="https://github.com/leejhlouis/louisite">https://github.com/leejhlouis/louisite</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lSjzXdvcVjOwEL2jKHdmSQ.png" /><figcaption>Google Lighthouse score for <a href="https://louisite.com">louisite.com</a> (recorded on May 28, 2024, 03:46:46 PM)</figcaption></figure><p>This article shows you how I built my portfolio site from scratch. Feel free to customize it to fit your needs. If you have suggestions to improve my portfolio site, I would appreciate them.</p><p>Thanks for reading. Happy coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=479778cf2960" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>