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
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
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
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;
Once added import this file in main.js:
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:
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 (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); }); }); }) ] };
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'), ] }
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>
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
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' : ''">
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
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>
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>
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>
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>
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>
Light & Dark mode results:
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