Skip to content

This project is meant to teach Vue.js fundamentals by creating a blog, step by step. It uses the Material Design / Vuetify framework and stores its data in the Google Firebase cloud.

Notifications You must be signed in to change notification settings

artingo/vue-blog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vue.js course

This project is meant to teach Vue.js fundamentals by creating a blog, step by step. It uses the Material Design / Vuetify framework and stores its data in the Google Firebase cloud.

Follow these steps to continuously build a client-side Blog web app with Vue.js.

1. Create project

  1. In your IDE, create a new Vue.js project:
    New project

  2. Create these folders and files:
    folders

  3. Create the application routes in src/router/index.js:

const routes = [
  {path: '/', component: Overview},
  {path: '/posts', component: Overview},
  {path: '/posts/:id', component: Read},
  {path: '/posts/create', component: Create},
  {path: '/posts/update', component: Overview},
  {path: '/posts/save', component: Overview},
  {path: '/posts/delete', component: Overview},
  {path: '/categories', component: Categories},
  {path: '/users', component: Users},
]
  1. Create and mount all necessary plugins and Vue in main.js:
    main.js code

2. Add props to PostCard

  1. To add props to the PostCard component, insert the following code:
const props = defineProps(['title', 'subtitle', 'avatar'])
  1. In Overview, create some test PostCards and pass the proper props.
const avatars = ref([
'/female_avatar.jpeg',
'/male_avatar.png',
'/user1-128x128.jpg',
])
<v-col v-for="i in 9" :key="i" cols="auto">
  <PostCard :title="'Post no ' + i"
    :subtitle="'Freddie on ' + new Date().toDateString()"
    :avatar="avatars[i % 3]">
      Lorum ipsum...
  </PostCard>
</v-col>

3. 'Create post' view

Create post screenshot

  1. In Create, add these form fields:
<v-text-field label="Title"  v-model="title" :rules="titleRules" required/>
<v-select label="Categories" v-model="categories" multiple
  :items="['Attractions', 'Beaches', 'Cities', 'Escape Rooms', 'Mountains', 'Museums']">
</v-select>
<v-textarea label="Content" v-model="body" :rules="bodyRules"></v-textarea>
  1. Add some model states and validation rules:
const title = ref('')
const titleRules = [(value) => value ? true : 'Please enter a post title']
const categories = ref([])
const body = ref('')
const bodyRules = [(value) => value ? true : 'Please enter some post content']

4. Load test postings from FireBase

  1. Follow these steps to connect your web app with FireBase.
  2. Create db.js a file, where you initialize your FireBase connecttion.
...
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
export default db
  1. Create a new FireStore database, a collection named 'posts' and some test documents.
    FireStore collection

  2. In Overview, load the FireStore data in the onMounted hook and store the postings in the posts state:

const posts = ref([])

onMounted(async () => await loadPostings())

async function loadPostings() {
  const postings = await getDocs(collection(db, "posts"))
  ...
  posts.value.push(currentPost)
}

5. Load categories from FireStore

Detail view

  1. In main.js, load the categories from FireStore:
async function loadCategories() {
  const catCollection = await getDocs(collection(db, "categories"))
  ...
}
  1. Provide them to all Vue components:
app.provide('categories', await loadCategories())
  1. In App, inject and use them as list items:
const categories = inject('categories')
...
<v-list-item v-for="category in categories">
  {{ category }}
</v-list-item>
  1. In Overview, pass the whole post object instead of title and body:
<PostCard :post="post" />
  1. In PostCard, replace title and body with props.post.
<v-card :title="props.post.title" :subtitle="subtitle">
  1. Inject the global categories and create a computed property to display the post's categories:
const categories = inject('categories')
const subtitle = computed(() => {
  const cats = props.post.categories?.map(cat => categories[cat])
  return cats?.join(', ')
})

6. 'Show details' feature

Detail view

  1. In the routes, enable dynamic route parameters:
{path: '/posts/:id', component: Read, props: true}
  1. In PostCard, add a dynamic link with the post.id.
<v-card ... link :to="'/posts/' + props.post.id">
  1. In Read, implement the posting 'detail view':
<script setup>
const props = defineProps(['id'])
const post = ref({})

onMounted(async () => await loadPost(props.id))

async function loadPost(id) {
  const postDoc = await getDoc(doc(db, "posts", id))
  ...
  post.value = {...postDoc.data()}
}
</script>
<template>
  ...
  <v-card :title="post.title" :subtitle="subtitle">
    ...
    <v-card-text>
      {{ post.body }}
    </v-card-text>
  </v-card>
</template>

7. 'Edit post' feature

Detail view

  1. In the routes, enable dynamic route parameters:
{path: '/posts/edit/:id', component: Edit, props: true}
  1. In Read, add a button with a link containing the post.id.
<v-btn color="primary" variant="elevated" link :to="'/posts/edit/' + props.id">
  Edit
</v-btn>
  1. Rename Create.vue to Edit.vue.
  2. For an existing post, load it's data using the ID:
onMounted(async () => {
  // only execute for existing posting
  if (props.id) await loadPost(props.id)
})

async function loadPost(id) {
  const post = await getDoc(doc(db, "posts", id))
  if (post.exists()) {
    isNewDoc.value = false
    title.value = post.data().title
    body.value = post.data().body
    categories.value = post.data().categories?.map(cat => cat.id)
  }
}
  1. The form should now look like this:
    Edit post form

  1. Write a savePost() function that stores the form data in FireStore:
async function savePost() {
    // if there is a validation error, abort
    if (!isValid.value) return false

    // load the FireStore references for selected categories
    const selectedCategories = categories.value?.map(catRef => doc(db, 'categories', catRef))
    const newPost = {
        title: title.value,
        body: body.value,
        categories: selectedCategories
    }
    const coll = collection(db, "posts")
    
    if (isNewDoc.value) {
        // create a new Posting in FireStore
        await addDoc(coll, newPost)
    } else {
        // update the existing Posting in FireStore
        await setDoc(doc(coll, props.id), newPost)
    }
    
    // forward to overview page
    router.push('/posts')
}

8. 'Delete post' feature

Delete button

  1. In Read, add a delete button:
<v-btn color="red-darken-4" variant="elevated" v-bind="props">
  Delete
</v-btn>
  1. Create a confirmation dialog:
    Delete confirm dialog
<v-dialog width="auto">
  <template v-slot:default="{ isActive }">
    <v-card>
      <v-toolbar color="red-darken-4" title="Delete posting"/>
      <v-card-text>
        <div class="text-h4 pa-8">
          Are you sure that you<br/>
          want to delete this post?
        </div>
      </v-card-text>
      <v-card-actions class="justify-space-between">
        <v-btn variant="elevated" color="grey-darken-1"
           @click="isActive.value = false">
          Cancel
        </v-btn>
        <v-btn variant="elevated" color="red-darken-4"
           @click="deletePost(props.id)">
          Yes, delete
        </v-btn>
      </v-card-actions>
    </v-card>
  </template>
</v-dialog>
  1. Write a deletePost() function that deletes the posting in FireStore:
async function deletePost(id) {
    await deleteDoc(doc(db, "posts", id))
    // forward to overview page
    router.push('/posts')
}

Project Setup

npm install

Compile and Hot-Reload for Development

npm run dev

Compile and Minify for Production

npm run build

About

This project is meant to teach Vue.js fundamentals by creating a blog, step by step. It uses the Material Design / Vuetify framework and stores its data in the Google Firebase cloud.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published