• Shallom Kyle Jacinto
April 21st 2021 • 5 minute read
Build a news app with progressive ui framework (vuejs) and next generation tool for frontend compiling (vitejs) and a utility first css framework (tailwind css).
A node.js installed in your computer.
A knowledge of Vue 2 or Vue 3
A basic knowledge of Tailwind CSS
To create a vue vite app, run ...
npm init @vitejs/app news-app
Then choose vue for framework and select javascript for variant
cd news-app
npm install
To install latest tailwind css version, run ...
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
To create a tailwind.config.js and postcss.config.js run ...
npx tailwindcss init -p
Inside in our tailwind.config.js, add this configuration ...
- purge: []
+ purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
This will remove all unused classes in tailwind.
And add index.css into ./src/assets/index.css and then inside in our css, add ...
@tailwind base;
@tailwind components;
@tailwind utilities;
And in your main.js, import the index.css
import './assets/index.css'
- node_modules
- public
- src
- assets
- index.css
- logo.png
- components
- ErrorCard.vue
- Loading.vue
- Navbar.vue
- NewsList.vue
- composition
- useFetch.js
- router
- index.js
- views
- Home.vue
- TopHeadlines.vue
- App.vue
- main.js
- .gitignore
- index.html
- package.json
- postcss.config.js
- tailwind.config.js
- vite.config.js
To create our routes, open a new tab of terminal and run ...
npm install vue-router@4
and create a new folder name routes and create a file `index.js
index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/top-headlines',
name: 'Top Headlines',
component: () => import('../views/TopHeadlines.vue')
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
Create a folder name views which will define all our pages and then create a two files, Home.vue and TopHeadlines.vue
Home.vue
<template>
<div>
<p>Home</p>
</div>
</template>
TopHeadlines.vue
<template>
<div>
<p>Top Headlines</p>
</div>
</template>
App.vue
<template>
<div>
<router-view />
</div>
</template>
And import the router on main.js
import router from './router/
...
.use(router)
To create our Navbar with router-links, we must create a new component Navbar.vue.
<template>
<div class="px-4 py-3 flex flex-wrap justify-between shadow-sm">
<div><span class="text-indigo-400">Vue 3</span> News</div>
<div>
<router-link exact to='/' class="mr-4">Home</router-link>
<router-link to="/top-headlines">Top Headlines</router-link>
</div>
</div>
</template>
<style>
.router-link-exact-active {
@apply text-indigo-400 border-b-2 border-indigo-500;
}
</style>
To add an active class, we put a exact attribute in home router to tell our vue-router that home is different with other paths and we add a .router-link-exact-active for styling.
App.vue
<template>
<Navbar />
<div class="px-4 py-4">
<router-view />
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
</script>
Navbar
In our app it's not a good practice that in every pages like fetching data in our home are the same on fetching data on other pages code, so to make our code maintainable, we create a folder composition which we will place all our reusable compositions and we create a file useFetch.js means this will be only use if there is fetching.
useFetch.js
import { toRefs, reactive } from 'vue'
export default function(url) {
const state = reactive({
data: [],
error: null,
fetching: false
})
const fetchData = async () => {
state.fetching = true
try {
await fetch(url)
.then(response => response.json())
.then(result => {
if(result.status == "error") {
state.error = result.message
} else {
state.data = result
}
})
}
catch(e) {
state.error = e
}
finally {
state.fetching = false
}
}
return {...toRefs(state), fetchData}
}
As you can see we pass the parameter in function url, we create our own state with reactive and an object of data which will contain all our data, error if there's an error, and fetching to tell our users that our data are still fetching or loading.
In our fetchData function, we set the fetching into true, we use a try catch block and inside in try block, we fetch the url and to get the data, we response it with json, we make an if else statement that if result is error then we set the error with message and else we set the data.
We return the state with toRefs means that we return our reactive object into plain object.
To get a news, first we must create an account in newsapi.org and after that go to account settings and copy your apiKey.
Home.vue
<template>
<div>
<div v-if="error" class="m-auto md:w-1/2">
<ErrorCard :error="error" />
</div>
<div v-else-if="fetching" class="m-auto md:w-1/2">
<Loading />
</div>
<div v-else class="m-auto md:w-1/2">
<NewsList :data="data.articles" />
</div>
</div>
</template>
<script>
import { onMounted, ref } from 'vue'
import useFetch from '../composition/useFetch'
import NewsList from '../components/NewsList.vue'
import Loading from '../components/Loading.vue'
import ErrorCard from '../components/ErrorCard.vue'
export default {
components: {
NewsList,
Loading,
ErrorCard
},
setup() {
const { fetchData, data, error, fetching } = useFetch('https://newsapi.org/v2/everything?q=news&apiKey=__your__api__key')
onMounted(async () => {
await fetchData()
})
return { fetchData, data, error, fetching }
}
}
</script>
As you can see we create a lifecycle hook onMounted and then we fetchData().
We use destructuring to get the keys, we import the useFetch, we pass the url api with apiKeys and then return all our state which is data,error, fetching.
We use v-if if there's an error, and then Display the loading if our app is still fetching and we pass our data as a props if fetching is done.
NewsList.vue
<template>
<div>
<div v-for="article in data" :key="article.id" class="p-4 bg-gray-50 mb-4 rounded-lg">
<img :src="article.urlToImage" class="rounded-lg w-full ">
<p class="mt-2 text-indigo-500 font-bold">{{ article.author }}</p>
<p class="tracking-wide text-gray-700 font-medium mt-1">{{ article.title }}</p>
<p class="mt-2 text-gray-700 text-sm">{{ article.description }}</p>
<a :href="article.url" target="_blank" class="inline-block mt-3 bg-indigo-500 text-white px-3 py-1.5 rounded">Read more</a>
</div>
</div>
</template>
<script>
export default {
props: {
data: Array
}
}
</script>
We loop our data props with v-for.
If fetching ...
If done fetching...
And for our Top Headlines route, we just copy all codes and replace our url into https://newsapi.org/v2/top-headlines?country=us&apiKey=your__api__key. Thats it!
For source code, open this github repo