DEV Community

Cover image for How to show your Github repos using Vue 3 and TailwindCSS
Alba Silvente Fuentes
Alba Silvente Fuentes

Posted on • Originally published at dawntraoz.com

How to show your Github repos using Vue 3 and TailwindCSS

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 ๐Ÿ’œ.

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 with the option that allows us to create an application with Vue 3. For that, we need @vue/cli v4.5:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

Once the project has been created we are going to add TailwindCSS following the official installation guide.

  • First, let's install tailwindcss running:
yarn add tailwindcss # npm install tailwindcss
Enter fullscreen mode Exit fullscreen mode
  • Then, create a css folder inside src/assets and add a file named tailwind.css with the content below:
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Once added import this file in main.js:

import "./assets/css/tailwind.css";
Enter fullscreen mode Exit fullscreen mode
  • Optional - Create our own Tailwind config file

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

npx tailwindcss init # will create a tailwind.config.js file
Enter fullscreen mode Exit fullscreen mode

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 (that you can easily install), but in my case I've only added one of the variants to my project .

    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);
            });
          });
        })
      ]
    };
Enter fullscreen mode Exit fullscreen mode
  • 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.

    module.exports = {
      plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
      ]
    }
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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

bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100
Enter fullscreen mode Exit fullscreen mode

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.

<div :class="darkMode ? 'dark-mode' : ''">
Enter fullscreen mode Exit fullscreen 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:

yarn add axios # npm install axios
Enter fullscreen mode Exit fullscreen mode

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, 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.
<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

Here is the complete code for this component:

<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>
Enter fullscreen mode Exit fullscreen mode

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

<script>
importย {ย isReactiveย }ย fromย "vue";

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

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:.

<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>
Enter fullscreen mode Exit fullscreen mode

Light & Dark mode results:

github repository card light mode

github repository card dark mode

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/

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

Demo: https://dawntraoz.github.io/github-repos/

Top comments (0)