فصل چهارم: صفحات و مسیریابی

در این فصل، به پیاده‌سازی صفحات اصلی و سیستم مسیریابی (Routing) اپلیکیشن مرکز اجاره کشتی‌های کروز با استفاده از Nuxt 3 می‌پردازیم. Nuxt از سیستم مسیریابی مبتنی بر فایل استفاده می‌کند که صفحات به‌صورت خودکار از ساختار پوشه pages/ تولید می‌شوند. همچنین، با استفاده از layouts، یک قالب کلی برای اپلیکیشن تعریف می‌کنیم و از قابلیت‌های پیشرفته مانند SEO و اعتبارسنجی فرم‌ها بهره می‌بریم. در ادامه، هر بخش به‌طور کامل توضیح داده شده و کدهای مربوطه شرح داده می‌شوند.


قالب کلی (Layout) - layouts/default.vue

قالب‌ها در Nuxt برای ایجاد ساختار مشترک بین صفحات استفاده می‌شوند. فایل default.vue به‌عنوان قالب پیش‌فرض برای تمام صفحات اپلیکیشن عمل می‌کند.

کد:

<!-- layouts/default.vue -->
<template>
  <div class="min-h-screen bg-gray-50">
    <header class="bg-blue-800 text-white p-4">
      <nav class="container mx-auto flex justify-between items-center">
        <NuxtLink to="/" class="text-2xl font-bold">Cruise Rental</NuxtLink>
        <div class="space-x-4">
          <NuxtLink to="/ships" class="hover:underline">Ships</NuxtLink>
          <NuxtLink to="/bookings" class="hover:underline">Bookings</NuxtLink>
        </div>
      </nav>
    </header>
    <main>
      <slot />
    </main>
  </div>
</template>

توضیحات:

  • ساختار قالب:

  • <div class="min-h-screen bg-gray-50">: یک ظرف اصلی با حداقل ارتفاع برابر با کل صفحه و پس‌زمینه خاکستری روشن.

  • <header>: شامل نوار ناوبری (Navigation Bar) با رنگ پس‌زمینه آبی تیره و متن سفید.
  • <nav>: شامل لینک‌های ناوبری به صفحات اصلی (صفحه اصلی، لیست کشتی‌ها، و رزروها) با استفاده از <NuxtLink> برای مسیریابی سمت کلاینت.
  • <main>: تگ <slot /> جایی است که محتوای صفحات خاص (مانند صفحه اصلی یا لیست کشتی‌ها) رندر می‌شود.
  • استایل‌دهی:

  • از Tailwind CSS برای استایل‌دهی استفاده شده است (مانند flex justify-between items-center برای تراز کردن آیتم‌های ناوبری).

  • کلاس space-x-4 فاصله افقی بین لینک‌های ناوبری ایجاد می‌کند.
  • افکت hover:underline هنگام حرکت ماوس روی لینک‌ها، خط زیرین اضافه می‌کند.
  • کاربرد: این قالب به تمام صفحات اپلیکیشن یک ساختار یکپارچه می‌دهد و ناوبری را در دسترس کاربران قرار می‌دهد.

صفحه اصلی (Homepage) - pages/index.vue

صفحه اصلی، صفحه فرود اپلیکیشن است که کاربران را به سمت مرور کشتی‌ها هدایت می‌کند و برای سئو بهینه‌سازی شده است.

کد:

<!-- pages/index.vue -->
<template>
  <div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-blue-100 to-gray-100">
    <h1 class="text-5xl font-bold mb-4 text-center">Cruise Ship Rental Center</h1>
    <p class="text-lg mb-8 text-center max-w-2xl">
      Discover luxury and adventure with our premium cruise ship rentals.
    </p>
    <NuxtLink to="/ships" class="btn text-lg">Browse Ships</NuxtLink>
  </div>
</template>

<script setup>
useSeoMeta({
  title: 'Cruise Ship Rental Center | Luxury Cruises 2025',
  description: 'Rent premium cruise ships for your next adventure. Explore our fleet and book today!',
  ogImage: '/og-image.jpg', // Add an image in public/
});
</script>

توضیحات:

  • بخش Template:

  • یک ظرف تمام‌صفحه با گرادیان پس‌زمینه از آبی روشن به خاکستری روشن (bg-gradient-to-b).

  • عنوان بزرگ (text-5xl) و پاراگراف توضیحی با عرض محدود (max-w-2xl) برای خوانایی.
  • دکمه "Browse Ships" با استفاده از کلاس سفارشی btn (تعریف‌شده در Tailwind CSS) که کاربران را به صفحه لیست کشتی‌ها هدایت می‌کند.
  • بخش Script:

  • از useSeoMeta برای تنظیم متادیتای سئو استفاده شده است:

    • title: عنوان صفحه برای نمایش در مرورگر و موتورهای جستجو.
    • description: توضیح مختصر برای بهبود سئو.
    • ogImage: تصویر Open Graph برای نمایش در شبکه‌های اجتماعی (تصویر باید در پوشه public/ قرار گیرد).
  • کاربرد: این صفحه کاربران را با اپلیکیشن آشنا می‌کند و با متادیتای سئو، رتبه‌بندی در موتورهای جستجو را بهبود می‌دهد.

لیست کشتی‌ها (Ships List) - pages/ships/index.vue

این صفحه لیستی از کشتی‌های موجود را با قابلیت فیلتر و صفحه‌بندی نمایش می‌دهد.

کد:

<!-- pages/ships/index.vue -->
<template>
  <div class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">Available Cruise Ships</h1>

    <!-- Filters -->
    <div class="mb-6 flex gap-4">
      <input v-model="search" placeholder="Search ships..." class="border p-2 rounded" />
      <input v-model.number="maxPrice" type="number" placeholder="Max price/day" class="border p-2 rounded" />
    </div>

    <!-- Ship Grid -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
      <ShipCard v-for="ship in filteredShips" :key="ship.id" :ship="ship" />
    </div>

    <!-- Pagination -->
    <div class="mt-6 flex justify-center gap-2">
      <button
        v-for="page in totalPages"
        :key="page"
        @click="shipsStore.setPage(page)"
        :class="['px-4 py-2 rounded', shipsStore.pagination.page === page ? 'bg-blue-500 text-white' : 'bg-gray-200']"
      >
        {{ page }}
      </button>
    </div>
  </div>
</template>

<script setup>
import { useShipsStore } from '~/stores/ships';
const shipsStore = useShipsStore();

const search = ref('');
const maxPrice = ref(Infinity);

watch(search, (value) => shipsStore.setSearch(value));
watch(maxPrice, (value) => shipsStore.setMaxPrice(value));

const filteredShips = computed(() => shipsStore.filteredShips);
const totalPages = computed(() => shipsStore.totalPages);

onMounted(() => {
  shipsStore.fetchShips();
});

useSeoMeta({
  title: 'Browse Cruise Ships | Rental Center 2025',
  description: 'Explore our fleet of luxury cruise ships available for rent.',
});
</script>

توضیحات:

  • بخش Template:

  • فیلترها: دو ورودی برای جستجوی نام کشتی (search) و فیلتر قیمت حداکثر (maxPrice) با استایل Tailwind.

  • گرید کشتی‌ها: از کامپوننت ShipCard برای نمایش کشتی‌ها در یک گرید پاسخ‌گو استفاده می‌شود (یک ستون در موبایل، سه ستون در دسکتاپ).
  • صفحه‌بندی: دکمه‌هایی برای هر صفحه که با کلیک روی آن‌ها، شماره صفحه در store به‌روزرسانی می‌شود.
  • بخش Script:

  • Store: از useShipsStore برای دسترسی به داده‌ها و متدهای store کشتی‌ها استفاده می‌شود.

  • Refs: متغیرهای search و maxPrice برای اتصال به ورودی‌های فیلتر.
  • Watchers: با تغییر search یا maxPrice، متدهای setSearch و setMaxPrice در store فراخوانی می‌شوند.
  • Computed Properties:

    • filteredShips: لیست کشتی‌های فیلترشده را از store می‌گیرد.
    • totalPages: تعداد کل صفحات را محاسبه می‌کند.
    • onMounted: هنگام بارگذاری صفحه، متد fetchShips برای دریافت داده‌های کشتی‌ها فراخوانی می‌شود.
    • SEO: متادیتا برای بهبود سئو تنظیم شده است.
    • کاربرد: این صفحه به کاربران امکان می‌دهد کشتی‌ها را جستجو، فیلتر و در صفحات مختلف مشاهده کنند.

جزئیات کشتی (Ship Details) - pages/ships/[id].vue

این صفحه جزئیات یک کشتی خاص را نمایش داده و فرمی برای رزرو آن ارائه می‌دهد.

کد:

<!-- pages/ships/[id].vue -->
<template>
  <div v-if="ship" class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-4">{{ ship.name }}</h1>
    <NuxtImg :src="ship.image" alt="Ship" class="w-full h-64 object-cover mb-4 rounded" />
    <p class="mb-2">Capacity: {{ ship.capacity }} passengers</p>
    <p class="mb-2">Price: ${{ ship.pricePerDay }}/day</p>
    <p class="mb-4">Amenities: {{ ship.amenities.join(', ') }}</p>

    <h2 class="text-2xl font-bold mb-4">Book This Ship</h2>
    <form @submit.prevent="submit" class="space-y-4">
      <div>
        <label class="block">Start Date</label>
        <input v-model="form.startDate" type="date" class="border p-2 rounded w-full" />
        <span v-if="errors.startDate" class="text-red-500">{{ errors.startDate }}</span>
      </div>
      <div>
        <label class="block">End Date</label>
        <input v-model="form.endDate" type="date" class="border p-2 rounded w-full" />
        <span v-if="errors.endDate" class="text-red-500">{{ errors.endDate }}</span>
      </div>
      <button type="submit" class="btn">Book Now</button>
    </form>
  </div>
  <div v-else>Ship not found.</div>
</template>

<script setup>
import { useShipsStore } from '~/stores/ships';
import { useBookingsStore } from '~/stores/bookings';
import { useForm } from 'vee-validate';
import * as yup from 'yup';

const route = useRoute();
const shipsStore = useShipsStore();
const bookingsStore = useBookingsStore();

const ship = computed(() => shipsStore.getShipById(Number(route.params.id)));

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, defineField } = useForm({ validationSchema: schema });
const [startDate] = defineField('startDate');
const [endDate] = defineField('endDate');
const form = reactive({ startDate: '', endDate: '' });

const submit = handleSubmit(async (values) => {
  if (!ship.value) return;
  const days = (new Date(values.endDate) - new Date(values.startDate)) / (1000 * 60 * 60 * 24);
  const totalPrice = days * ship.value.pricePerDay;
  await bookingsStore.addBooking({
    shipId: ship.value.id,
    startDate: new Date(values.startDate),
    endDate: new Date(values.endDate),
    totalPrice,
  });
  alert('Booking successful!');
  form.startDate = '';
  form.endDate = '';
});

useSeoMeta({
  title: () => ship.value ? `${ship.value.name} | Cruise Rental 2025` : 'Ship Details',
  description: () => ship.value ? `Book ${ship.value.name} for your next adventure!` : 'View cruise ship details.',
});
</script>

توضیحات:

  • بخش Template:

  • اگر کشتی یافت شود (v-if="ship"):

    • نمایش نام، تصویر (با <NuxtImg> برای بهینه‌سازی)، ظرفیت، قیمت روزانه و امکانات.
    • فرم رزرو با دو ورودی برای تاریخ شروع و پایان و نمایش خطاها (errors) با رنگ قرمز.
    • دکمه "Book Now" با کلاس btn.
  • در غیر این صورت، پیام "Ship not found" نمایش داده می‌شود.
  • بخش Script:

  • وابستگی‌ها: استفاده از storeهای ships و bookings، و کتابخانه‌های vee-validate و yup برای اعتبارسنجی.

  • مسیریابی: useRoute برای دریافت id از URL.
  • داده کشتی: ship با استفاده از getShipById از store دریافت می‌شود.
  • اعتبارسنجی فرم:

    • از yup برای تعریف اسکیما استفاده شده که تاریخ شروع را اجباری و بعد از امروز، و تاریخ پایان را بعد از تاریخ شروع الزام می‌کند.
    • useForm و defineField برای مدیریت فرم و خطاها.
  • ارسال فرم: متد submit تعداد روزهای رزرو را محاسبه کرده، قیمت کل را تعیین می‌کند و رزرو را به store اضافه می‌کند.

  • SEO: متادیتای پویا بر اساس نام کشتی تنظیم شده است.
  • کاربرد: این صفحه امکان مشاهده جزئیات و رزرو یک کشتی خاص را فراهم می‌کند.

داشبورد رزروها (Bookings Dashboard) - pages/bookings.vue

این صفحه تاریخچه رزروهای کاربر را نمایش می‌دهد.

کد:

<!-- pages/bookings.vue -->
<template>
  <div class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">Your Bookings</h1>
    <ul v-if="bookings.length" class="space-y-4">
      <li v-for="booking in bookings" :key="booking.id" class="card p-4">
        <p>Ship: {{ getShipName(booking.shipId) }}</p>
        <p>Dates: {{ formatDate(booking.startDate) }} to {{ formatDate(booking.endDate) }}</p>
        <p>Total: ${{ booking.totalPrice }}</p>
      </li>
    </ul>
    <p v-else>No bookings yet.</p>
  </div>
</template>

<script setup>
import { useBookingsStore } from '~/stores/bookings';
import { useShipsStore } from '~/stores/ships';

const bookingsStore = useBookingsStore();
const shipsStore = useShipsStore();

const bookings = computed(() => bookingsStore.bookings);

const getShipName = (shipId: number) => {
  const ship = shipsStore.getShipById(shipId);
  return ship ? ship.name : 'Unknown';
};

const formatDate = (date: Date) => new Date(date).toLocaleDateString();

onMounted(() => {
  bookingsStore.fetchBookings();
});

useSeoMeta({
  title: 'Your Bookings | Cruise Rental 2025',
  description: 'View and manage your cruise ship bookings.',
});
</script>

توضیحات:

  • بخش Template:

  • اگر رزرو وجود داشته باشد، لیستی از رزروها با استفاده از کلاس card نمایش داده می‌شود.

  • هر رزرو شامل نام کشتی، تاریخ‌ها و قیمت کل است.
  • در غیر این صورت، پیام "No bookings yet" نمایش داده می‌شود.
  • بخش Script:

  • Storeها: استفاده از useBookingsStore و useShipsStore برای دسترسی به رزروها و اطلاعات کشتی‌ها.

  • Computed: bookings لیست رزروها را از store دریافت می‌کند.
  • getShipName: نام کشتی را بر اساس shipId پیدا می‌کند.
  • formatDate: تاریخ‌ها را به فرمت خوانا تبدیل می‌کند.
  • onMounted: هنگام بارگذاری صفحه، رزروها از سرور دریافت می‌شوند.
  • SEO: متادیتا برای بهبود سئو تنظیم شده است.
  • کاربرد: این صفحه به کاربران امکان می‌دهد رزروهای خود را مشاهده و مدیریت کنند.

جمع‌بندی فصل چهارم

در این فصل، صفحات اصلی اپلیکیشن (صفحه اصلی، لیست کشتی‌ها، جزئیات کشتی، و داشبورد رزروها) را با استفاده از سیستم مسیریابی Nuxt پیاده‌سازی کردیم. از قالب پیش‌فرض برای یکپارچگی، Tailwind CSS برای استایل‌دهی، و قابلیت‌های سئو و اعتبارسنجی برای بهبود تجربه کاربری استفاده شد. در فصل‌های بعدی، به کامپوننت‌ها و مسیرهای سرور برای تکمیل اپلیکیشن خواهیم پرداخت.