keyboard_tabSkip to content
listVue Firebase Masterclass

ทำส่วน Layout หลักของหน้าเว็บกัน

สารบัญ

เราจะทำอะไรกันบ้างในฝั่ง User

intro-01

นี่คือทั้งหมดที่เราจะทำกันใน Session นี้

├── src
    ├── App.vue
    ├── components
    ├── style.css
    ├── layouts
       ├── UserLayout.vue --> สำหรับ navbar และ footer
    ├── stores
       ├── event.js --> สำหรับเพ่ิม event ที่ใช้รวม
       ├── user
           ├── product.js --> สำหรับเก็บข้อมูล product
           └── cart.js --> สำหรับเก็บข้อมูลตะกร้าสินค้า
    └── views
        └── user --> folder สำหรับ user view
            ├── CartView.vue --> หน้าตะกร้าสินค้า
            ├── CheckoutView.vue --> หน้าชำระเงิน
            ├── HomeView.vue --> หน้าหลัก
            ├── ProfileView.vue --> หน้า Profile
            ├── SearchView.vue --> หน้าค้นหาสินค้า
            └── SuccessView.vue --> หน้าชำระเงินสำเร็จ

โดย ฝั่งของหน้าเว็บ ที่ user ใช้งานทั้งหมดเราจะมีหน้าตามนี้คือ

  1. Home = หน้าหลักที่ user เปิดมาเจอหน้าแรก
  2. Profile = หน้าจัดการ profile ที่ user สามารถแก้ไข profile ตัวเองได้
  3. Cart = หน้าตะกร้าสินค้าที่จะเข้ามาหลังจากเลือกสินค้าแล้ว
  4. Checkout = หน้าต่อจากตะกร้าสินค้าที่เป็นการสรุปยอดรวม
  5. Search = หน้าค้นหาสินค้า
  6. Success = หลังจาก checkout เรียบร้อย (หลังชำระเงิน) แสดงหน้า ชำระเงินสำเร็จออกมา
Loading diagram…

และนี่คือหน้าภาพรวมทั้งหมดที่เราจะทำกัน ไล่ตั้งแต่หน้า Home > Search > Profile > Cart > Checkout และ Success

เริ่มต้นทำ UserLayout.vue

layouts/UserLayout.vue คือ component สำหรับสร้าง Layout หลักของหน้า User เอาไว้ โจทย์ของ component นี้คือ

  • เราจะสร้างส่วนหัวของเว็บไซต์ และส่วนท้ายของเว็บไซต์ที่ไม่ว่าเราเปิดจากหน้าไหน จะต้องใช้งานร่วมกัน = ทำเป็น Parent component ของตัวนั้นไว้
  • เราจะใช้ concept <slot></slot> ในการแทรก component ที่เป็น component ของตัวลูกที่เราจะใช้ร่วมกันอีกที

เช่นเคสตามเอกสาร Vue นี้

  • เราสร้าง FancyButton ที่สามารถแทรกตรงกลางเข้าไปได้
  • เมื่อไหร่ก็ตามที่เราใส่อะไรเอาไว้ระหว่าง <FancyButton></FancyButton> = มันจะมาแทนตรง slot ของ component นั้นได้
  • concept นี้มันจะเอาไว้ใช้ทำสิ่งที่เรียกกันว่า template content คือการที่ component เป็นตัวหลักในการแสดงผล และเอาสิ่งอื่นมาแทรกตรงกลางแทน

layout-04

เอกสารต้นฉบับ https://vuejs.org/guide/components/slots.html#slots

checklist ที่เราจะทำ

  • สร้าง style โดยเพิ่ม navbar เข้ามาที่ประกอบไปด้วย
    • หัวข้อ logo (ด้านซ้ายสุด)
    • search box (ที่วางไว้ก่อนยังไม่เพิ่ม action อะไร)
    • icon ตะกร้า พร้อมจำนวนสินค้า
    • Login
  • เพิ่ม login / logout โดย
    • ตอนแรกสุด ให้แสดงเป็นคำว่า Login
    • หลังจากกด Login ให้เปลี่ยนเป็น Icon รูปคนและสามารถกดเมนู Profile และ Logout ออกมาดูได้
    • ทั้งหมดใช้ localstorage ในการจำ state (เนื่องจากยังไม่มี Backend)
    • เมื่อกด logout ให้กลับมาแสดงคำว่า Login และล้าง state เหมือนเดิม
  • เพิ่ม pop-up ตรงตะกร้าสินค้ามา
    • เมื่อกด pop-up จะแสดงราคารวมของสินค้าและไปยังหน้าตะกร้า
  • เพิ่ม footer เข้าไป (โดยให้ดูทรงเป็นเว็บไซต์จริงๆเฉยๆ)
  • เพิ่ม <slot></slot> เข้าไป เพื่อ mark จุดลงใน component ในแต่ละหน้า = ทุกหน้าจะมาแทรกกลางระหว่าง header (navbar) และ footer นี้
html
<script setup>
import { ref, onMounted } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
const isLoggedIn = ref(false)

const router = useRouter()

onMounted(() => {
  if (localStorage.getItem('login')) {
    isLoggedIn.value = true
  }
})

const login = () => {
  isLoggedIn.value = true
  localStorage.setItem('login', true)
}

const logout = () => {
  isLoggedIn.value = false
  localStorage.removeItem('login')
  localStorage.removeItem('cart-item')
  localStorage.removeItem('checkout-data')
  window.location.reload()
}
</script>

<template>
  <div class="max-w-screen-xl mx-auto">
    <div class="navbar bg-base-100">
      <div class="flex-1">
        <RouterLink to="/" class="btn btn-ghost normal-case text-xl">Mikelopster Shop</RouterLink>
      </div>
      <div class="flex-none">
        <div class="form-control">
          <input
            type="text"
            v-model="searchText"
            placeholder="Search"
            class="input input-bordered w-24 md:w-auto"
          />
        </div>
        <div class="dropdown dropdown-end">
          <label tabindex="0" class="btn btn-ghost btn-circle">
            <div class="indicator">
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
              <span class="badge badge-sm indicator-item">0</span>
            </div>
          </label>
          <div tabindex="0" class="mt-3 z-[1] card card-compact dropdown-content w-52 bg-base-100 shadow">
            <div class="card-body">
              <span class="font-bold text-lg">0 Items</span>
              <span class="text-info">Subtotal: 100฿</span>
              <div class="card-actions">
                <RouterLink to="/cart" class="btn btn-primary btn-block">
                  View cart
                </RouterLink>
              </div>
            </div>
          </div>
        </div>
        <div v-if="!isLoggedIn" class="btn btn-ghost" @click="login">
          Login
        </div>
        <div v-else class="dropdown dropdown-end">
          <label tabindex="0" class="btn btn-ghost btn-circle avatar">
            <div class="w-10 rounded-full">
              <img src="https://mikelopster.dev/mikelopster.da6b9a03.webp" />
            </div>
          </label>
          <ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
            <li>
              <RouterLink to="/profile" class="justify-between">
                Profile
              </RouterLink>
            </li>
            <li>
              <a @click="logout">Logout</a>
            </li>
          </ul>
        </div>
      </div>
    </div>
    <slot></slot>
    <footer class="footer p-10 bg-neutral text-neutral-content">
      <div>
        <span class="footer-title">Services</span> 
        <a class="link link-hover">Branding</a>
        <a class="link link-hover">Design</a>
        <a class="link link-hover">Marketing</a>
        <a class="link link-hover">Advertisement</a>
      </div> 
      <div>
        <span class="footer-title">Company</span> 
        <a class="link link-hover">About us</a>
        <a class="link link-hover">Contact</a>
        <a class="link link-hover">Jobs</a>
        <a class="link link-hover">Press kit</a>
      </div> 
      <div>
        <span class="footer-title">Legal</span> 
        <a class="link link-hover">Terms of use</a>
        <a class="link link-hover">Privacy policy</a>
        <a class="link link-hover">Cookie policy</a>
      </div>
    </footer>
  </div>
</template>

ผลลัพธ์

state ปกติ layout-01

state login แล้ว (หลังจากกดปุ่ม Login) > กดเมนูตรงรูป layout-02

เมื่อกดตะกร้า > แสดงเมนูตะกร้าออกมา layout-03

เพิ่ม router และสร้าง template ทุกหน้าขึ้นมา

เพิ่ม router สำหรับ file ทุกหน้าของ user โดยมีหน้าดังนี้

  • / = หน้า Home
  • /search = หน้า Search (สำหรับค้นหา Product)
  • /profile = หน้า Profile (สำหรับแก้ไข Profile)
  • /cart = หน้า Cart (สำหรับดูสินค้าที่เอาเข้าตะกร้าสินค้า)
  • /checkout = หน้า Checkout (สำหรับหน้าสรุปตะกร้าสินค้า)
  • /success = หน้า Success (จบรายการหลังจาก Checkout)

และทำการสร้าง template เปล่าให้กับทุก file ของ .vue ของแต่ละหน้า (HomeView.vue, SearchView.vue, ProfileView.vue, SuccessView.vue, CheckoutView.vue, CartView.vue)

javascript
import Home from '@/views/user/HomeView.vue'
import Search from '@/views/user/SearchView.vue'
import Profile from '@/views/user/ProfileView.vue'
import Success from '@/views/user/SuccessView.vue'
import Checkout from '@/views/user/CheckoutView.vue'
import Cart from '@/views/user/CartView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/search',
      name: 'search',
      component: Search
    },
    {
      path: '/profile',
      name: 'profile',
      component: Profile
    },
    {
      path: '/cart',
      name: 'cart',
      component: Cart
    },
    {
      path: '/checkout',
      name: 'checkout',
      component: Checkout
    },
    {
      path: '/success',
      name: 'success',
      component: Success
    },
  ]
})

export default router