Frontend
Components
Component conventions and patterns across all Booki frontend apps.
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Component files | PascalCase.vue | CustomerNavbar.vue, BookingCard.vue |
| Shared (codi-layer) | Prefix-free, consumed via layer auto-import | AlertBanner.vue |
| App-specific | Usually prefixed by app domain | CustomerBookingList.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, nodefineComponent. - Type your props with
defineProps<Props>()generics — not the object syntax. - Type your emits with
defineEmits<{...}>()generics. withDefaultsfor default prop values.- Use
v-bindshorthand (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.
Sidebar layout (owner / admin)
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>
Navbar layout (customer)
<!-- 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" />