ضمیمه ج: جزئیات کامل پیاده‌سازی کامپوننت‌های اضافی (فصل ۵)

این ضمیمه جزئیات کامل درباره کامپوننت‌های اضافی که در فصل پنجم به‌صورت خلاصه یا غایب بودند را ارائه می‌دهد. تمرکز بر پیاده‌سازی کامپوننت‌های AppHeader.vue (هدر) و BookingForm.vue (فرم رزرو جداگانه) و همچنین نحوه استایل‌دهی آن‌ها با Tailwind CSS است. این کامپوننت‌ها به اپلیکیشن مرکز اجاره کشتی‌های کروز قابلیت‌های ماژولار و کاربرپسند اضافه می‌کنند و از اصول طراحی پاسخ‌گو و مدرن پیروی می‌کنند.


کامپوننت هدر (AppHeader.vue)

هدر اپلیکیشن برای ناوبری و ارائه یک تجربه کاربری یکپارچه طراحی شده است. در فصل پنجم، هدر به‌صورت پیش‌فرض در layouts/default.vue پیاده‌سازی شده بود، اما تبدیل آن به یک کامپوننت جداگانه امکان استفاده مجدد و نگهداری بهتر را فراهم می‌کند.

کد کامل:

<!-- components/AppHeader.vue -->
<template>
  <header class="bg-blue-800 text-white p-4 sticky top-0 z-10">
    <nav class="container mx-auto flex justify-between items-center">
      <!-- لوگو و لینک به صفحه اصلی -->
      <NuxtLink to="/" class="text-2xl font-bold tracking-tight">
        Cruise Rental
      </NuxtLink>
      <!-- لینک‌های ناوبری -->
      <div class="space-x-4 flex items-center">
        <NuxtLink to="/ships" class="hover:underline hover:text-blue-200 transition duration-200">
          Ships
        </NuxtLink>
        <NuxtLink to="/bookings" class="hover:underline hover:text-blue-200 transition duration-200">
          Bookings
        </NuxtLink>
        <!-- دکمه ورود/خروج -->
        <template v-if="authData">
          <span class="text-sm">{{ authData.user?.name }}</span>
          <button @click="signOut" class="btn btn-sm bg-red-500 hover:bg-red-600">
            Sign Out
          </button>
        </template>
        <NuxtLink v-else to="/login" class="btn btn-sm">
          Sign In
        </NuxtLink>
      </div>
    </nav>
  </header>
</template>

<script setup>
import { useAuth, signOut } from '#auth';

const { data: authData } = useAuth();
</script>

توضیحات:

  • ساختار:

  • <header>: از کلاس‌های Tailwind مانند bg-blue-800, text-white, و sticky top-0 برای ایجاد یک هدر ثابت با پس‌زمینه آبی تیره استفاده می‌کند.

  • <nav>: شامل لوگو (لینک به /) و لینک‌های ناوبری به صفحات /ships و /bookings.
  • ورود/خروج: با استفاده از useAuth از @sidebase/nuxt-auth، وضعیت احراز هویت بررسی می‌شود. اگر کاربر وارد شده باشد، نام کاربر و دکمه خروج نمایش داده می‌شود؛ در غیر این صورت، لینک ورود ظاهر می‌شود.
  • استایل‌دهی:

  • کلاس‌های Tailwind مانند hover:underline, transition duration-200, و btn (تعریف‌شده در assets/css/main.css) برای جلوه‌های بصری و پاسخ‌گویی.

  • container mx-auto: محتوا را در مرکز صفحه با حاشیه‌های مناسب نگه می‌دارد.
  • flex justify-between items-center: برای چیدمان افقی و تراز عمودی لینک‌ها.
  • کاربرد:

  • این کامپوننت در layouts/default.vue استفاده می‌شود تا در تمام صفحات نمایش داده شود:

    <!-- layouts/default.vue -->
    <template>
      <div class="min-h-screen bg-gray-50">
        <AppHeader />
        <main class="container mx-auto p-6">
          <slot />
        </main>
      </div>
    </template>
  • مزایا:

  • ماژولاریتی: جداسازی هدر به‌عنوان یک کامپوننت مستقل، نگهداری و گسترش را آسان می‌کند.

  • پاسخ‌گویی: طراحی با Tailwind برای دستگاه‌های مختلف پاسخ‌گو است.
  • ادغام با احراز هویت: نمایش پویا بر اساس وضعیت ورود کاربر.

کامپوننت فرم رزرو (BookingForm.vue)

فرم رزرو که در pages/ships/[id].vue استفاده شده بود، می‌تواند به یک کامپوننت جداگانه تبدیل شود تا قابلیت استفاده مجدد و کد تمیزتر فراهم شود. این کامپوننت از vee-validate برای اعتبارسنجی فرم استفاده می‌کند.

کد کامل:

<!-- components/BookingForm.vue -->
<template>
  <form @submit.prevent="submit" class="space-y-4 max-w-md mx-auto">
    <div>
      <label class="block text-sm font-medium text-gray-700">Start Date</label>
      <input
        v-model="form.startDate"
        type="date"
        class="border p-2 rounded w-full focus:ring focus:ring-blue-200"
      />
      <span v-if="errors.startDate" class="error">{{ errors.startDate }}</span>
    </div>
    <div>
      <label class="block text-sm font-medium text-gray-700">End Date</label>
      <input
        v-model="form.endDate"
        type="date"
        class="border p-2 rounded w-full focus:ring focus:ring-blue-200"
      />
      <span v-if="errors.endDate" class="error">{{ errors.endDate }}</span>
    </div>
    <button type="submit" class="btn w-full">Book Now</button>
  </form>
</template>

<script setup>
import { useForm } from 'vee-validate';
import * as yup from 'yup';
import type { Ship } from '~/types';

defineProps<{ ship: Ship }>();
defineEmits<{
  (e: 'submit', values: { startDate: string; endDate: string }): void;
}>();

const schema = yup.object({
  startDate: yup
    .date()
    .required('Start date is required')
    .min(new Date(), 'Start date must be today or later'),
  endDate: yup
    .date()
    .required('End date is required')
    .min(yup.ref('startDate'), 'End date must be after start date'),
});

const { handleSubmit, errors } = useForm({ validationSchema: schema });
const form = reactive({ startDate: '', endDate: '' });

const submit = handleSubmit((values) => {
  emit('submit', values);
});
</script>

استفاده در pages/ships/[id].vue:

<!-- pages/ships/[id].vue -->
<script setup>
import { useBookingsStore } from '~/stores/bookings';
import type { Ship } from '~/types';
import { loadStripe } from '@stripe/stripe-js';

const route = useRoute();
const bookingsStore = useBookingsStore();
const config = useRuntimeConfig();

const { data: ship } = await useAsyncData('ship', () => $fetch(`/api/ships/${route.params.id}`));

const handleBooking = async (values: { startDate: string; endDate: string }) => {
  if (!ship.value) return;
  const days = (new Date(values.endDate) - new Date(values.startDate)) / (1000 * 60 * 60 * 24);
  const totalPrice = days * ship.value.pricePerDay;
  const booking = {
    shipId: ship.value.id,
    startDate: new Date(values.startDate),
    endDate: new Date(values.endDate),
    totalPrice,
  };

  const { id } = await $fetch('/api/stripe-checkout', {
    method: 'POST',
    body: { booking },
  });

  const stripe = await loadStripe(config.public.stripePublishableKey);
  await stripe.redirectToCheckout({ sessionId: id });

  await bookingsStore.addBooking(booking);
  alert('Booking successful!');
};
</script>

<template>
  <div v-if="ship" class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">{{ ship.name }}</h1>
    <NuxtImg :src="ship.image" alt="Ship" class="w-full h-64 object-cover mb-4 rounded" />
    <BookingForm :ship="ship" @submit="handleBooking" />
  </div>
</template>

توضیحات:

  • ساختار:

  • <form>: شامل دو فیلد ورودی برای تاریخ شروع و پایان، با اعتبارسنجی توسط vee-validate و Yup.

  • فیلدها:
    • startDate: تاریخ شروع رزرو، باید امروز یا آینده باشد.
    • endDate: تاریخ پایان رزرو، باید پس از startDate باشد.
  • دکمه: دکمه "Book Now" با کلاس btn برای ارسال فرم.
  • استایل‌دهی:

  • کلاس‌های Tailwind مانند space-y-4, max-w-md, و mx-auto برای چیدمان و پاسخ‌گویی.

  • focus:ring focus:ring-blue-200: افکت فوکوس برای ورودی‌ها.
  • error: کلاس سفارشی (تعریف‌شده در assets/css/main.css) برای نمایش خطاها.
  • اعتبارسنجی:

  • از Yup برای تعریف قوانین اعتبارسنجی استفاده شده است:

    • required: فیلدها اجباری هستند.
    • min(new Date()): تاریخ شروع باید امروز یا آینده باشد.
    • min(yup.ref('startDate')): تاریخ پایان باید پس از تاریخ شروع باشد.
  • خطاها با کلاس error نمایش داده می‌شوند.
  • کاربرد:

  • این کامپوننت در ships/[id].vue استفاده می‌شود تا فرم رزرو را به‌صورت جداگانه مدیریت کند.

  • رویداد submit داده‌های فرم را به صفحه والد (ships/[id].vue) ارسال می‌کند تا برای محاسبه قیمت و ارسال به Stripe استفاده شود.
  • مزایا:

  • ماژولاریتی: جداسازی فرم رزرو کد را تمیزتر و قابل نگهداری‌تر می‌کند.

  • اعتبارسنجی قوی: استفاده از vee-validate خطاهای کاربر را به‌صورت بلادرنگ نمایش می‌دهد.
  • پاسخ‌گویی: طراحی با Tailwind برای دستگاه‌های مختلف مناسب است.

استایل‌دهی اضافی با Tailwind CSS

برای اطمینان از یکپارچگی بصری در کامپوننت‌ها، استایل‌های سفارشی در assets/css/main.css تعریف شده‌اند:

@tailwind base;
@tailwind components;
@tailwind utilities;

.card {
  @apply bg-white shadow-md rounded-lg overflow-hidden;
}
.btn {
  @apply bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition duration-200;
}
.btn-sm {
  @apply px-3 py-1 text-sm;
}
.error {
  @apply text-red-500 text-sm mt-1;
}
  • کلاس‌های استفاده‌شده:

  • .card: برای کامپوننت‌هایی مانند ShipCard و کارت‌های رزرو در bookings.vue.

  • .btn و .btn-sm: برای دکمه‌ها در AppHeader و BookingForm.
  • .error: برای نمایش خطاهای اعتبارسنجی در BookingForm.
  • پاسخ‌گویی:

  • در AppHeader.vue:

    @media (max-width: 640px) {
      .space-x-4 {
        @apply flex-col space-y-2 space-x-0;
      }
    }
- این کد باعث می‌شود لینک‌های ناوبری در دستگاه‌های کوچک به‌صورت عمودی نمایش داده شوند.
  • در BookingForm.vue:
    • max-w-md mx-auto فرم را در دستگاه‌های بزرگ متمرکز و محدود به عرض متوسط می‌کند.

چرا این بخش در ضمیمه‌ها خلاصه بود؟

ضمیمه‌های اولیه فقط به کامپوننت ShipCard.vue اشاره کردند و کامپوننت‌های اضافی مانند AppHeader.vue و BookingForm.vue را پوشش ندادند. همچنین، جزئیات استایل‌دهی و ادغام این کامپوننت‌ها با احراز هویت و فرم‌های پویا به‌صورت کامل توضیح داده نشده بود.


نکات عملی برای پیاده‌سازی

  1. ایجاد کامپوننت‌ها:

  2. فایل‌های AppHeader.vue و BookingForm.vue را در پوشه components/ ایجاد کنید.

  3. اطمینان حاصل کنید که assets/css/main.css شامل کلاس‌های سفارشی است.

  4. ادغام با احراز هویت:

  5. در AppHeader.vue، از @sidebase/nuxt-auth برای بررسی وضعیت ورود استفاده کنید.

  6. مسیر /login را برای هدایت کاربران غیرمجاز تنظیم کنید.

  7. تست پاسخ‌گویی:

  8. با ابزار DevTools مرورگر، نمایش کامپوننت‌ها را در اندازه‌های مختلف صفحه (موبایل، تبلت، دسکتاپ) بررسی کنید.

  9. از npm run dev برای تست بلادرنگ استفاده کنید.

  10. گسترش:

  11. در AppHeader.vue، می‌توانید منوی همبرگری برای موبایل اضافه کنید:

     <button class="md:hidden" @click="toggleMenu">☰</button>
     <div v-if="menuOpen" class="md:hidden flex flex-col space-y-2 mt-2">
       <NuxtLink to="/ships">Ships</NuxtLink>
       <NuxtLink to="/bookings">Bookings</NuxtLink>
     </div>
     <script setup>
     const menuOpen = ref(false);
     const toggleMenu = () => { menuOpen.value = !menuOpen.value; };
     </script>
  • در BookingForm.vue، فیلدهای اضافی مانند تعداد مسافران اضافه کنید:
     const schema = yup.object({
       startDate: yup.date().required().min(new Date()),
       endDate: yup.date().required().min(yup.ref('startDate')),
       passengers: yup.number().required('Number of passengers is required').min(1),
     });

جمع‌بندی ضمیمه ج

این ضمیمه جزئیات کامل پیاده‌سازی کامپوننت‌های AppHeader.vue و BookingForm.vue را ارائه داد، شامل کد، استایل‌دهی با Tailwind CSS، و ادغام با احراز هویت و اعتبارسنجی فرم. این کامپوننت‌ها اپلیکیشن را ماژولارتر و کاربرپسندتر می‌کنند. برای اطلاعات بیشتر، به فصل‌های ۵ و ۱۱ مراجعه کنید یا مخزن فرضی GitHub (https://github.com/khosronz/cruise-rental-app) را بررسی کنید.