How to show your Github repos using Vue 3 and TailwindCSS
Tags: vue3, tailwindcss, github, darkmode
Published: 2020-10-29 19:43:09

In this post I would like to show you how I have added Github repositories to my website using Vue 3 and TailwindCSS, also with dark mode 💜.

{% tweet 1321894086576668678 %}

## Creating a Vue 3 app with Jest, ESLint and TailwindCSS

As I really wanted to start with Vue 3, I decided to create a new repository as a demo of this post, just to try it out.

> I've also started using yarn for this project, but I will keep the npm equivalencies, just in case 😂.

In order not to complicate my installations I have used [Vue CLI](https://cli.vuejs.org/guide/installation.html) with the option that allows us to create an application with Vue 3. For that, we need @vue/cli v4.5:

```bash
yarn global add @vue/cli # npm install -g @vue/cli

# OR if you already have it

vue --version # lower than 4.5? Upgrade! ⤵
yarn global upgrade --latest @vue/cli # npm update -g @vue/cli
```

Now we are ready to create the project with a simple command!

```bash
vue create github-repos

## Manually select features
# Option 1 - Babel, linter, unit testing
# Option 2 - 3.x
# Option 3 - ESLint + Prettier
# Option 4 - Lint and fix on commit
# Option 5 - Jest
# Option 6 - In dedicated config files
```

Once the project has been created we are going to add TailwindCSS following [the official installation guide](https://tailwindcss.com/docs/installation).

- First, let's install tailwindcss running:

```bash
yarn add tailwindcss # npm install tailwindcss
```

- Then, create a *css* folder inside *src/assets* and add a file named *tailwind.css* with the content below:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

  Once added import this file in main.js:

```jsx
import "./assets/css/tailwind.css";
```

- **Optional** - Create our own Tailwind config file

  As I want to customize Tailwind installation, I need to generate a config file running:

```bash
npx tailwindcss init # will create a tailwind.config.js file
```

  Once it was generated, I wanted to add some padding to the container and center it. In addition, I decided to set purgecss and dark mode, extending screens with (prefers-color-scheme: dark) and the new variant dark to textColor & backgroundColor.

> Then, I realise I want to show the effect by clicking a button. So I checked this awesome plugin: [https://github.com/ChanceArthur/tailwindcss-dark-mode](https://github.com/ChanceArthur/tailwindcss-dark-mode) (that you can easily install), but in my case I've only added one of the variants to my project .

```jsx
    const plugin = require("tailwindcss/plugin");
    const selectorParser = require("postcss-selector-parser");

    module.exports = {
      future: {},
      purge: ["./src/**/*.vue"],
      theme: {
        extend: {
          screens: {
            dark: { raw: "(prefers-color-scheme: dark)" }
          }
        },
        container: {
          center: true,
          padding: "1rem"
        }
      },
      variants: {
        textColor: ["dark", "responsive", "hover", "focus"],
        backgroundColor: ["dark", "responsive", "hover", "focus"]
      },
      plugins: [
        plugin(function({ addVariant }) {
          addVariant("dark", ({ modifySelectors, separator }) => {
            modifySelectors(({ selector }) => {
              return selectorParser(selectors => {
                selectors.walkClasses(sel => {
                  sel.value = `dark${separator}${sel.value}`;
                  sel.parent.insertBefore(
                    sel,
                    selectorParser().astSync(".dark-mode ")
                  );
                });
              }).processSync(selector);
            });
          });
        })
      ]
    };
```

- Finally, to process your CSS with Tailwind we need to configure PostCSS

  Create a file named *postcss.config.js* and add tailwindcss & autoprefixer as plugins.

```jsx
    module.exports = {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ]
    }
```

Now we are ready to see the main goal of this post!! Let's show our GitHub Repos to the world 🚀

## GithubRepos & GithubReposItem components

### 1st step - Update App.vue

To start with, I always go to App.vue, where we find the component HelloWorld.vue. In this case, as my aim is to create a simple component and not a complete application, I just changed *HelloWorld* to **GithubRepos** and added a button to **toggle** between *light/dark* mode:

```jsx
<template>
  <div :class="darkMode ? 'dark-mode' : ''">
    <div
      class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100"
    >
      <header class="container pt-4 text-right">
        <button
          @click="darkMode = !darkMode"
          class="focus:outline-none pb-1 border-b"
        >
          Toggle to {{ darkMode ? "light" : "dark" }} mode
        </button>
      </header>
      <GithubRepos />
    </div>
  </div>
</template>

<script>
import GithubRepos from "./components/GithubRepos.vue";

export default {
  name: "App",
  data() {
    return {
      darkMode: false
    };
  },
  components: {
    GithubRepos
  }
};
</script>
```

As you can see here, to set up dark text and background colors I've added the **dark:** breakpoint ⤵

```html
bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100
```

And to be able to see light colors change to dark ones when clicking at the button, I have added the *.dark-mode* class (defined in plugins at tailwind.config.js) in the first div.

```html
<div :class="darkMode ? 'dark-mode' : ''">
```

### 2nd step - Create GithubRepos.vue and use axios to call Github API

Now the fun stuff starts! As I had to make calls to an API I was not going to hesitate to use my beloved promise based HTTP client **axios**.

I just added axios to my project to start making calls:

```bash
yarn add axios # npm install axios
```

Once added, I only had to import it and start creating the calls. I decided to group each call in a method, let's see them:

- **getColors -** It should be called first, because when we represent the languages that have been used in the repository we will need to associate them with their official color in Github. For that, at the mounted hook I made getRepos to wait until getColors has finished.
- **getRepos -** In this case we only need to do one get to our url per Github user [https://api.github.com/users/{user}/repos](https://api.github.com/users/%7Buser%7D/repos), we can do the operations we want with the result obtained. I have chosen to **filter** out the repositories that I have not created myself (**fork** = true), **order** them by the majority of **stars** (stargazers_count) and keep only the **first 6**.

```jsx
<script>
import axios from "axios";

export default {
  data() {
    return {
      repos: [],
      colors: {}
    };
  },
  methods: {
    getColors() {
      axios
        .get(
          "https://raw.githubusercontent.com/ozh/github-colors/master/colors.json"
        )
        .then(res => (this.colors = res.data));
    },
    getRepos() {
      axios.get("https://api.github.com/users/dawntraoz/repos").then(res => {
        this.repos = res.data
          .filter(repo => !repo.fork)
          .sort(
            (repo1, repo2) => repo2.stargazers_count - repo1.stargazers_count
          )
          .slice(0, 6);
      });
    },
  },
  async mounted() {
    await this.getColors();
    this.getRepos();
  },
};
</script>
```

To show the results I've created a responsive grid containing the github cards defined in GithubReposItem.vue.

```html
<template>
  <div class="container">
    <!-- Header title -->
    <header class="pt-4">
      <h2 class="font-bold text-lg md:text-2xl">
        Github Repositories
      </h2>
    </header>
    <!-- Grid mobile: 1, tablet: 2, desktop: 3 columns -->
    <div v-if="repos && repos.length > 0" class="flex flex-wrap pt-4 md:-mr-6">
      <div
        v-for="repo in repos"
        :key="repo.id"
        class="w-full md:w-1/2 lg:w-1/3 md:pr-6 pb-4"
      >
        <GithubReposItem
          :repository="repo"
          :bg-color="repo.language ? colors[repo.language].color : '#ffffff'"
        />
      </div>
    </div>
  </div>
</template>
```

Here is the complete code for this component:

```jsx
<template>
  <div class="container">
    <header class="pt-4">
      <h2 class="font-bold text-lg md:text-2xl">
        Github Repositories
      </h2>
    </header>
    <div v-if="repos && repos.length > 0" class="flex flex-wrap pt-4 md:-mr-6">
      <div
        v-for="repo in repos"
        :key="repo.id"
        class="w-full md:w-1/2 lg:w-1/3 md:pr-6 pb-4"
      >
        <GithubReposItem
          :repository="repo"
          :bg-color="repo.language ? colors[repo.language].color : '#ffffff'"
        />
      </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";
import GithubReposItem from "./GithubReposItem.vue";

export default {
  name: "GithubRepos",
  data() {
    return {
      repos: [],
      colors: {}
    };
  },
  methods: {
    getColors() {
      axios
        .get(
          "https://raw.githubusercontent.com/ozh/github-colors/master/colors.json"
        )
        .then(res => (this.colors = res.data));
    },
    getRepos() {
      axios.get("https://api.github.com/users/dawntraoz/repos").then(res => {
        this.repos = res.data
          .filter(repo => !repo.fork)
          .sort(
            (repo1, repo2) => repo2.stargazers_count - repo1.stargazers_count
          )
          .slice(0, 6);
      });
    }
  },
  async mounted() {
    await this.getColors();
    this.getRepos();
  },
  components: {
    GithubReposItem
  }
};
</script>
```

> **Curiosity:** When you return an object from data() in a component, it is internally made reactive by reactive(). The template is compiled into a render function that makes use of these reactive properties. - By [https://v3.vuejs.org/guide/reactivity-fundamentals.html#declaring-reactive-state](https://v3.vuejs.org/guide/reactivity-fundamentals.html#declaring-reactive-state)

```jsx
<script>
import { isReactive } from "vue";

export default {
  name: "GithubRepos",
  data() {
    return {
      repos: []
    };
  },
  // ...
  mounted() {
    console.log(isReactive(this.repos)); // return true
  },
}
</script>
```

### 3rd step - Create GithubReposItem.vue

Now, the repositories come with a lot of interesting data, but I have chosen the ones that are usually shown:

- Name and description
- Number of stars (*stargazers_count*)
- Forked times (*forks*)
- Mostly used language (*language*)

Working with flex classes we can align the elements and with SVGs icons we can give it a fun touch 😍. Below you can have a look at the code and the result in light & dark mode, taking into account texts and backgrounds defined with *dark:*.

```jsx
<template>
  <div class="border h-full rounded p-4 flex flex-col">
    <div class="flex items-center">
      <svg viewBox="0 0 16 16" class="w-4 h-4 fill-current mr-2" aria-hidden="true">
        <path fill-rule="evenodd" :d="icon.book"></path>
      </svg>
      <a :href="repository.html_url" target="_blank" class="font-medium text-purple-800 dark:text-purple-200">
        {{ repository.name }}
      </a>
    </div>
    <div class="text-xs mt-2 mb-4">
      {{ repository.description }}
    </div>
    <div class="mt-auto text-xs flex">
      <div v-if="repository.language" class="flex items-center mr-4">
        <span
          :style="{ backgroundColor: repository.language ? bgColor : '' }"
          class="w-3 h-3 rounded-full relative"
        ></span>
        <span class="pl-2">{{ repository.language }}</span>
      </div>
      <div v-if="repository.stargazers_count" class="flex items-center mr-4">
        <svg class="w-4 h-4 fill-current mr-2" aria-label="stars" viewBox="0 0 16 16" role="img">
          <path fill-rule="evenodd" :d="icon.star"></path>
        </svg>
        <span>{{ repository.stargazers_count }}</span>
      </div>
      <div v-if="repository.forks" class="flex items-center">
        <svg class="w-4 h-4 fill-current mr-2" aria-label="fork" viewBox="0 0 16 16" role="img">
          <path fill-rule="evenodd" :d="icon.fork"></path>
        </svg>
        <span>{{ repository.forks }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "GithubReposItem",
  props: {
    repository: {
      type: Object,
      default: () => {
        return {};
      }
    },
    bgColor: {
      type: String,
      default: undefined
    }
  },
  data: () => ({
    icon: {
      book:
        "M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z",
      star:
        "M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z",
      fork:
        "M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"
    }
  })
};
</script>
```

Light & Dark mode results:

![github repository card light mode](https://dev-to-uploads.s3.amazonaws.com/i/hxpyitzxqxwjtfniqemj.png)

![github repository card dark mode](https://dev-to-uploads.s3.amazonaws.com/i/q73h9r7u7gp27j7fi84s.png)

## Testing components & CI

As some of you may know, I have been putting **unit tests** and continuous integration **workflows** into my projects lately. As this could not be less, I have left you the tests and the workflow in the repository!

For those of you who don't know what a workflow is, I recommend you to look at this post:  [https://www.dawntraoz.com/blog/how-to-add-ci-to-frontend-project-with-github-actions/](https://www.dawntraoz.com/blog/how-to-add-ci-to-frontend-project-with-github-actions/)

I hope you like it and, above all, that it helps you 💜

## Fields of improvement

Obviously, there is much to improve but little by little :)
Here I leave you some ideas of how I would like to improve it:

- Abstract the logic of the API.
- Create an atomic component to represent SVG icons.
- Add an animation while waiting for the results.

If you want to collaborate, it's open source! Here I leave you the repository, if you want to make a PR with improvements, refactoring, new features I'm waiting for you 💜

Repository: [https://github.com/Dawntraoz/github-repos](https://github.com/Dawntraoz/github-repos) 

Demo: [https://dawntraoz.github.io/github-repos/](https://dawntraoz.github.io/github-repos/)