Frontend

Components

Component conventions and patterns across all Booki frontend apps.

Naming Conventions

TypeConventionExample
Component filesPascalCase.vueCustomerNavbar.vue, BookingCard.vue
Shared (codi-layer)Prefix-free, consumed via layer auto-importAlertBanner.vue
App-specificUsually prefixed by app domainCustomerBookingList.vue, OwnerCalendar.vue

Component Structure

All components use the Composition API with <script setup>. The script block always comes before the template:

<script lang="ts" setup>
import type { IBooking } from '~/types'

interface Props {
  booking: IBooking
  showActions?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showActions: true,
})

const emit = defineEmits<{
  cancel: [id: string]
  reschedule: [id: string]
}>()

function handleCancel() {
  emit('cancel', props.booking._id)
}
</script>

<template>
  <div class="card bg-base-100 shadow">
    <div class="card-body">
      <h2 class="card-title">{{ booking.service?.name }}</h2>
      <p class="text-sm text-base-content/70">{{ booking.bookingDate }}</p>
      <div v-if="showActions" class="card-actions justify-end">
        <button class="btn btn-sm btn-error" @click="handleCancel">Cancel</button>
      </div>
    </div>
  </div>
</template>

Key Rules

  • Always use <script lang="ts" setup> — no Options API, no defineComponent.
  • Type your props with defineProps<Props>() generics — not the object syntax.
  • Type your emits with defineEmits<{...}>() generics.
  • withDefaults for default prop values.
  • Use v-bind shorthand (v-bind="obj") when spreading attributes to child components.
  • Avoid logic in templates — move it to <script setup> or composables.

Layouts

Layouts live in app/layouts/. Each app has at minimum a default.vue.

Uses DaisyUI drawer for the sidebar:

<!-- app/layouts/default.vue (owner-booki-web-app) -->
<template>
  <div class="drawer lg:drawer-open min-h-screen">
    <input id="my-drawer-2" type="checkbox" class="drawer-toggle" />

    <div class="drawer-content flex flex-col">
      <!-- Mobile top bar with hamburger -->
      <div class="navbar lg:hidden bg-base-100 border-b">
        <label for="my-drawer-2" class="btn btn-ghost">
          <PhList class="w-5 h-5" />
        </label>
      </div>

      <main class="bg-base-100 flex-1 px-7 py-6">
        <slot />
      </main>
    </div>

    <!-- Sidebar -->
    <div class="drawer-side z-50">
      <label for="my-drawer-2" class="drawer-overlay" />
      <aside class="w-64 min-h-screen bg-base-200 p-4">
        <NavigationItems />
      </aside>
    </div>
  </div>
</template>
<!-- app/layouts/default.vue (customer-booki-web-app) -->
<template>
  <header class="sticky top-0 z-50 bg-white duration-300" :class="{ shadow: isScrolled }">
    <CustomerNavbar />
  </header>
  <main>
    <slot />
  </main>
</template>

<script lang="ts" setup>
const isScrolled = ref(false)

onMounted(() => window.addEventListener('scroll', handleScroll))
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))

function handleScroll() {
  isScrolled.value = window.scrollY > 0
}
</script>

Icon System

Icons come from Phosphor Icons (@phosphor-icons/vue). They are registered globally in codi-layer/app/plugins/phosphor-icons.ts:

import { PhEye, PhCaretLeft, PhMagnifyingGlass, PhList } from '@phosphor-icons/vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('PhEye', PhEye)
  nuxtApp.vueApp.component('PhCaretLeft', PhCaretLeft)
  nuxtApp.vueApp.component('PhMagnifyingGlass', PhMagnifyingGlass)
  nuxtApp.vueApp.component('PhList', PhList)
  // ...
})

Usage in templates:

<PhMagnifyingGlass class="w-5 h-5 text-base-content/50" />