Repository: Mutari-App/mutari-frontend
Pull request #190: PBI 34 detail tiket tur
Author: login: Nsyay
Created: 2025-05-09T10:29:16Z | Merged: 2025-05-12T15:25:45Z

# Changes:
- Built the Detail Tour page layout.
- Created reusable components such as `TourHeader`, `TourDescription`, `TourOrderCard` and `TourList`
- Combined and integrated the components within the page
- Implemented unit tests to ensure components render correctly and handle props as expected

# Notes:
- tour "Jadwal" and tour "services/includes" sections have not been implemented yet
![SSSSS](https://github.com/user-attachments/assets/e3a94dfa-34b9-4a4c-868a-74fa44a3cc0d)



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a detailed tour page displaying tour information, including title, location, cover image, description, price per ticket, and itinerary sections.
  - Added interactive components for tour description (expand/collapse), itinerary timeline, and order card with booking button.
  - Dynamic Open Graph metadata is now set when sharing or inviting to an itinerary.

- **Tests**
  - Added comprehensive test suites for all new tour detail components, covering rendering, UI behavior, and user interactions.

- **Documentation**
  - Defined TypeScript interfaces for all tour-related data structures to ensure type safety and clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/__tests__/TourDay.test.tsx b/__tests__/TourDay.test.tsx
new file mode 100644
index 00000000..9dcdb97f
--- /dev/null
+++ b/__tests__/TourDay.test.tsx
@@ -0,0 +1,97 @@
+import { render, screen } from '@testing-library/react'
+import { TourDay } from '@/modules/DetailTourModule/module-elements/TourDay'
+import { TransportMode } from '@/utils/maps'
+
+jest.mock('lucide-react', () => ({
+  MapPin: () => <div data-testid="map-pin" />,
+  Clock: () => <div data-testid="clock-icon" />,
+}))
+
+const mockSection = {
+  id: 'section1',
+  itineraryId: 'ITN-123',
+  sectionNumber: 1,
+  updatedAt: '2022-04-03T10:00:00Z',
+  createdAt: '2022-04-03T10:00:00Z',
+  title: 'Section 1',
+  contingencyPlanId: null,
+  blocks: [
+    {
+      id: 'block1',
+      updatedAt: '2022-04-03T10:00:00Z',
+      createdAt: '2022-04-03T10:00:00Z',
+      title: 'Block Title 1',
+      description: 'Block Description 1',
+      sectionId: 'section1',
+      position: 1,
+      blockType: 'LOCATION',
+      startTime: '2022-04-03T10:00:00Z',
+      endTime: '2022-04-03T12:00:00Z',
+      location: '0,0',
+      price: 100,
+      photoUrl: 'https://example.com/photo1.jpg',
+      routeToNext: {
+        id: 'route1',
+        createdAt: '2022-04-03T12:00:00Z',
+        updatedAt: '2022-04-03T12:00:00Z',
+        sourceBlockId: 'block1',
+        destinationBlockId: 'block2',
+        distance: 5000,
+        duration: 3600,
+        polyline: 'encoded_polyline_data',
+        transportMode: TransportMode.DRIVE,
+      },
+    },
+    {
+      id: 'block2',
+      updatedAt: '2022-04-03T14:00:00Z',
+      createdAt: '2022-04-03T14:00:00Z',
+      title: 'Block Title 2',
+      description: 'Block Description 2',
+      sectionId: 'section1',
+      position: 2,
+      blockType: 'NOTE',
+      startTime: '',
+      endTime: '',
+      location: '',
+      price: 0,
+      photoUrl: '',
+    },
+  ],
+}
+
+const formatTime = (time: string) =>
+  new Date(time).toLocaleTimeString('id-ID', {
+    hour: '2-digit',
+    minute: '2-digit',
+    hour12: false,
+  })
+
+describe('TourDay', () => {
+  it('renders MapPin icon when sectionNumber is 1', () => {
+    render(<TourDay section={mockSection} />)
+    expect(screen.getByTestId('map-pin')).toBeInTheDocument()
+  })
+
+  it('renders the section title', () => {
+    render(<TourDay section={mockSection} />)
+    expect(screen.getByText('Section 1')).toBeInTheDocument()
+  })
+
+  it('renders LOCATION block with title, times, and description', () => {
+    render(<TourDay section={mockSection} />)
+
+    const start = formatTime(mockSection.blocks[0].startTime)
+    const end = formatTime(mockSection.blocks[0].endTime)
+
+    expect(screen.getByText('Block Title 1')).toBeInTheDocument()
+    expect(screen.getByText('Block Description 1')).toBeInTheDocument()
+    expect(screen.getByTestId('clock-icon')).toBeInTheDocument()
+    expect(screen.getByText(`${start} - ${end}`)).toBeInTheDocument()
+  })
+
+  it('renders NOTE block with description only', () => {
+    render(<TourDay section={mockSection} />)
+    expect(screen.getByText('Block Description 2')).toBeInTheDocument()
+  })
+})
diff --git a/__tests__/TourDescription.test.tsx b/__tests__/TourDescription.test.tsx
new file mode 100644
index 00000000..754607b0
--- /dev/null
+++ b/__tests__/TourDescription.test.tsx
@@ -0,0 +1,52 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { TourDescription } from '@/modules/DetailTourModule/module-elements/TourDescription'
+
+jest.mock('lucide-react', () => ({
+  UsersRound: () => <div data-testid="users-icon" />,
+}))
+
+const mockProps = {
+  title: ' Amazing Bali Tour',
+  description: 'blublublublub',
+  maxCapacity: 12,
+}
+
+describe('TourDescription', () => {
+  it('renders title and maxCapacity text', () => {
+    render(<TourDescription {...mockProps} />)
+    expect(screen.getByText('Amazing Bali Tour')).toBeInTheDocument()
+    expect(screen.getByTestId('users-icon')).toBeInTheDocument()
+    expect(screen.getByText(/Open trip sampai 12 orang/)).toBeInTheDocument()
+  })
+
+  it('shows short description by default', () => {
+    render(<TourDescription {...mockProps} />)
+    const shortDesc =
+      mockProps.description.split(' ').slice(0, 30).join(' ') + '...'
+    expect(screen.getByText(shortDesc)).toBeInTheDocument()
+    expect(screen.getByText('Tampilkan deskripsi')).toBeInTheDocument()
+  })
+
+  it('toggles to full description on button click', () => {
+    render(<TourDescription {...mockProps} />)
+    const button = screen.getByRole('button', { name: 'Tampilkan deskripsi' })
+
+    fireEvent.click(button)
+
+    expect(screen.getByText(mockProps.description)).toBeInTheDocument()
+    expect(screen.getByText('Sembunyikan deskripsi')).toBeInTheDocument()
+  })
+
+  it('toggles back to short description on second click', () => {
+    render(<TourDescription {...mockProps} />)
+    const button = screen.getByRole('button', { name: 'Tampilkan deskripsi' })
+
+    fireEvent.click(button) // Expand
+    fireEvent.click(button) // Collapse
+
+    const shortDesc =
+      mockProps.description.split(' ').slice(0, 30).join(' ') + '...'
+    expect(screen.getByText(shortDesc)).toBeInTheDocument()
+    expect(screen.getByText('Tampilkan deskripsi')).toBeInTheDocument()
+  })
+})
diff --git a/__tests__/TourHeader.test.tsx b/__tests__/TourHeader.test.tsx
new file mode 100644
index 00000000..a1c792c7
--- /dev/null
+++ b/__tests__/TourHeader.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen } from '@testing-library/react'
+import { TourHeader } from '@/modules/DetailTourModule/module-elements/TourHeader'
+
+const mockTour = {
+  title: 'Amazing Bali Tour',
+  location: 'Bali, Indonesia',
+  coverImage: 'https://example.com/bali.jpg',
+}
+
+describe('TourHeader', () => {
+  it('renders title and location correctly', () => {
+    render(
+      <TourHeader
+        title={mockTour.title}
+        location={mockTour.location}
+        coverImage={mockTour.coverImage}
+      />
+    )
+    expect(screen.getByText('Amazing Bali Tour')).toBeInTheDocument()
+    expect(screen.getByText('Bali, Indonesia')).toBeInTheDocument()
+  })
+
+  it('applies the correct background image when coverImage is provided', () => {
+    const { container } = render(
+      <TourHeader
+        title={mockTour.title}
+        location={mockTour.location}
+        coverImage={mockTour.coverImage}
+      />
+    )
+    const backgroundDiv = container.querySelector('div.relative')
+    expect(backgroundDiv).toHaveStyle(
+      `background-image: url(${mockTour.coverImage})`
+    )
+  })
+
+  it('applies gradient background when coverImage is not provided', () => {
+    const mockTourNoImage = {
+      ...mockTour,
+      coverImage: undefined,
+    }
+    const { container } = render(
+      <TourHeader
+        title={mockTourNoImage.title}
+        location={mockTourNoImage.location}
+        coverImage={mockTourNoImage.coverImage || ''}
+      />
+    )
+    const backgroundDiv = container.querySelector('div.relative')
+    expect(backgroundDiv).toHaveStyle(
+      'background-image: linear-gradient(360deg, #004080, #0073E6, #60A5FA)'
+    )
+  })
+})
diff --git a/__tests__/TourList.test.tsx b/__tests__/TourList.test.tsx
new file mode 100644
index 00000000..f9a6c520
--- /dev/null
+++ b/__tests__/TourList.test.tsx
@@ -0,0 +1,75 @@
+import { render, screen } from '@testing-library/react'
+import { TourList } from '@/modules/DetailTourModule/module-elements/TourList'
+import { TransportMode } from '@/utils/maps'
+import '@testing-library/jest-dom'
+
+jest.mock('@/modules/DetailTourModule/module-elements/TourDay', () => ({
+  TourDay: ({ section }: { section: { title: string } }) => (
+    <div data-testid="tour-day">{section.title}</div>
+  ),
+}))
+
+const mockSections = [
+  {
+    id: 'section1',
+    itineraryId: 'ITN-001',
+    sectionNumber: 1,
+    title: 'Arrival Day',
+    updatedAt: '2022-04-03T10:00:00Z',
+    createdAt: '2022-04-03T10:00:00Z',
+    contingencyPlanId: null,
+    blocks: [
+      {
+        id: 'block1',
+        title: 'Airport',
+        description: 'Arrival at the airport',
+        updatedAt: '2022-04-03T10:00:00Z',
+        createdAt: '2022-04-03T10:00:00Z',
+        sectionId: 'section1',
+        position: 1,
+        blockType: 'LOCATION',
+        startTime: '2022-04-03T10:00:00Z',
+        endTime: '2022-04-03T12:00:00Z',
+        location: '0,0',
+        price: 0,
+        photoUrl: '',
+        routeToNext: {
+          id: 'route1',
+          createdAt: '2022-04-03T12:00:00Z',
+          updatedAt: '2022-04-03T12:00:00Z',
+          sourceBlockId: 'block1',
+          destinationBlockId: 'block2',
+          distance: 1000,
+          duration: 600,
+          polyline: '',
+          transportMode: TransportMode.WALK,
+        },
+      },
+    ],
+  },
+  {
+    id: 'section2',
+    itineraryId: 'ITN-001',
+    sectionNumber: 2,
+    title: 'Tour Day',
+    updatedAt: '2022-04-04T10:00:00Z',
+    createdAt: '2022-04-04T10:00:00Z',
+    contingencyPlanId: null,
+    blocks: [],
+  },
+]
+
+describe('TourList', () => {
+  it('renders nothing when section is empty', () => {
+    const { container } = render(<TourList section={[]} />)
+    expect(container.firstChild).toBeNull()
+  })
+
+  it('renders TourDay components for each section', () => {
+    render(<TourList section={mockSections} />)
+    const tourDays = screen.getAllByTestId('tour-day')
+    expect(tourDays).toHaveLength(2)
+    expect(tourDays[0]).toHaveTextContent('Arrival Day')
+    expect(tourDays[1]).toHaveTextContent('Tour Day')
+  })
+})
diff --git a/__tests__/TourOrderCard.test.tsx b/__tests__/TourOrderCard.test.tsx
new file mode 100644
index 00000000..4e754741
--- /dev/null
+++ b/__tests__/TourOrderCard.test.tsx
@@ -0,0 +1,16 @@
+import { render, screen } from '@testing-library/react'
+import { TourOrderCard } from '@/modules/DetailTourModule/module-elements/TourOrderCard'
+
+describe('TourOrderCard', () => {
+  it('displays the price per ticket correctly', () => {
+    render(<TourOrderCard pricePerTicket={250000} />)
+    expect(screen.getByText('Rp250.000')).toBeInTheDocument()
+    expect(screen.getByText('/ pax')).toBeInTheDocument()
+  })
+
+  it('renders the "Pesan Sekarang" button', () => {
+    render(<TourOrderCard pricePerTicket={250000} />)
+    const button = screen.getByRole('button', { name: /Pesan Sekarang/i })
+    expect(button).toBeInTheDocument()
+  })
+})
diff --git a/src/app/(main)/tour/[id]/page.tsx b/src/app/(main)/tour/[id]/page.tsx
new file mode 100644
index 00000000..544dc4c8
--- /dev/null
+++ b/src/app/(main)/tour/[id]/page.tsx
@@ -0,0 +1,23 @@
+import notFound from '@/app/not-found'
+import DetailTourModule from '@/modules/DetailTourModule'
+import { customFetch } from '@/utils/newCustomFetch'
+
+export default async function TourDetailPage({
+  params,
+}: Readonly<{
+  params: Promise<{ id: string }>
+}>) {
+  try {
+    const res = await customFetch<TourDetailResponse>(
+      `/tour/${(await params).id}`
+    )
+
+    if (res.statusCode === 404 || res.statusCode === 403) {
+      return notFound()
+    }
+
+    return <DetailTourModule initialData={res.data} />
+  } catch (err) {
+    return notFound()
+  }
+}
diff --git a/src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx b/src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx
index 21785fc1..d0c27518 100644
--- a/src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx
+++ b/src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx
@@ -15,6 +15,7 @@ import { customFetch } from '@/utils/newCustomFetch'
 import { toast } from 'sonner'
 import { DuplicateItineraryResponse } from '@/modules/ItineraryModule/module-elements/types'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import Head from 'next/head'
 
 export const ItineraryHeader = ({
   data,
@@ -343,6 +344,13 @@ export const ItineraryHeader = ({
           setShowInviteDialog(open)
         }}
       >
+        {(showModal || showInviteDialog) && (
+          <Head>
+            <meta property="og:title" content={data.title} />
+            <meta property="og:description" content={data.description || 'Lihat itinerary ini!'} />
+            <meta property="og:image" content={data.coverImage || '/default-image.jpg'} />
+          </Head>
+        )}
         <DialogContent className="font-roboto cursor-default p-6 pb-10 max-w-md">
           <DialogHeader>
             <DialogTitle className="text-2xl text-center w-full font-semibold">
diff --git a/src/modules/DetailTourModule/index.tsx b/src/modules/DetailTourModule/index.tsx
new file mode 100644
index 00000000..3f96bf2d
--- /dev/null
+++ b/src/modules/DetailTourModule/index.tsx
@@ -0,0 +1,44 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useState } from 'react'
+import { Loader2 } from 'lucide-react'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule({
+  initialData,
+}: {
+  initialData: Tour
+}) {
+  const [data] = useState<Tour>(initialData)
+
+  return data ? (
+    <div className="max-w-6xl mx-auto p-4 pt-24 py-8">
+      <TourHeader
+        title={data.title}
+        location={data.location}
+        coverImage={data.coverImage}
+      />
+      <div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
+        <div className="lg:col-span-2 space-y-6">
+          <TourDescription
+            title={data.title}
+            description={data.description}
+            maxCapacity={data.maxCapacity}
+          />
+          <TourInclude includes={data.includes} />
+        </div>
+        <div className="lg:col-span-1">
+          <TourOrderCard pricePerTicket={data.pricePerTicket} />
+        </div>
+        <TourList section={data.itinerary.sections} />
+      </div>
+    </div>
+  ) : (
+    <div className="flex items-center justify-center h-screen">
+      <Loader2 className="animate-spin w-6 h-6 mr-2" />
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourDay.tsx b/src/modules/DetailTourModule/module-elements/TourDay.tsx
new file mode 100644
index 00000000..7f54253f
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourDay.tsx
@@ -0,0 +1,66 @@
+import { cn } from '@/lib/utils'
+import { Clock, MapPin } from 'lucide-react'
+
+export const TourDay = ({ section }: { section: Section }) => {
+  return (
+    <div className="relative p-4 bg-white -mb-8">
+      <div className="absolute -left-1 top-5 bottom-8 w-0.5 bg-[#94A3B8]"></div>
+      <div
+        className={`absolute -left-5 top-5 bg-[#024C98] text-white font-bold w-8 h-12 flex items-center justify-center`}
+      >
+        {section.sectionNumber === 1 ? (
+          <MapPin className="w-5 h-5" />
+        ) : (
+          section.sectionNumber
+        )}
+      </div>
+      <h2 className="md:text-lg text-raleway mt-3 font-semibold ml-2">
+        {section.title}
+      </h2>
+      {section.blocks.map((block, index) => {
+        const isLastBlock = index === section.blocks.length - 1
+        const formatTime = (time: string) =>
+          time
+            ? new Date(time).toLocaleTimeString('id-ID', {
+                hour: '2-digit',
+                minute: '2-digit',
+                hour12: false,
+              })
+            : null
+        return (
+          <div key={block.id}>
+            <div
+              className={cn(
+                'p-3 w-full',
+                block.blockType === 'NOTE' ? 'bg-[#E5F1FF]' : 'bg-white'
+              )}
+            >
+              {block.blockType === 'LOCATION' && (
+                <>
+                  <h3 className="md:text-lg font-bold font-raleway">
+                    {block.title}
+                  </h3>
+                  <div className="flex items-center gap-4 md:text-sm text-[#024C98] font-roboto font-medium">
+                    {block.startTime && block.endTime && (
+                      <div className="flex items-center gap-1">
+                        <Clock size={16} /> {formatTime(block.startTime)} -{' '}
+                        {formatTime(block.endTime)}
+                      </div>
+                    )}
+                  </div>
+                  <p className="md:text-sm font-roboto">{block.description}</p>
+                </>
+              )}
+              {block.blockType === 'NOTE' && (
+                <p className="md:text-sm font-roboto">{block.description}</p>
+              )}
+              {isLastBlock && (
+                <div className="absolute -left-1 bottom-8 w-4 border-t-2 border-[#94A3B8]"></div>
+              )}
+            </div>
+          </div>
+        )
+      })}
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourDescription.tsx b/src/modules/DetailTourModule/module-elements/TourDescription.tsx
new file mode 100644
index 00000000..634f91e4
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourDescription.tsx
@@ -0,0 +1,39 @@
+'use client'
+
+import { useState } from 'react'
+import { UsersRound } from 'lucide-react'
+
+export const TourDescription = ({
+  title,
+  description,
+  maxCapacity,
+}: {
+  title: string
+  description: string
+  maxCapacity: number
+}) => {
+  const [expanded, setExpanded] = useState(false)
+  const shortDesc = description.split(' ').slice(0, 30).join(' ') + '...'
+
+  return (
+    <div className="text-sm text-gray-700 space-y-2">
+      <h2 className="text-base font-semibold">{title}</h2>
+
+      <div className="flex items-center gap-2 text-gray-600 text-sm">
+        <UsersRound className="text-gray-500" />
+        <span>Open trip sampai {maxCapacity} orang</span>
+      </div>
+
+      <p className="whitespace-pre-line">
+        {expanded ? description : shortDesc}
+      </p>
+
+      <button
+        onClick={() => setExpanded(!expanded)}
+        className="text-blue-600 hover:underline mt-1"
+      >
+        {expanded ? 'Sembunyikan deskripsi' : 'Tampilkan deskripsi'}
+      </button>
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourHeader.tsx b/src/modules/DetailTourModule/module-elements/TourHeader.tsx
new file mode 100644
index 00000000..9e6d0e03
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourHeader.tsx
@@ -0,0 +1,26 @@
+export const TourHeader = ({
+  title,
+  location,
+  coverImage,
+}: {
+  title: string
+  location: string
+  coverImage: string
+}) => {
+  return (
+    <div className="mb-4">
+      <h1 className="text-2xl font-bold">{title}</h1>
+      <p className="text-gray-500 mb-4">{location}</p>
+      <div
+        className="relative w-full h-40 md:h-64 rounded-md mb-4 flex items-center justify-center overflow-hidden"
+        style={{
+          backgroundImage: coverImage
+            ? `url(${coverImage})`
+            : 'linear-gradient(360deg, #004080, #0073E6, #60A5FA)',
+          backgroundSize: 'cover',
+          backgroundPosition: 'center',
+        }}
+      ></div>
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourInclude.tsx b/src/modules/DetailTourModule/module-elements/TourInclude.tsx
new file mode 100644
index 00000000..23aebf3b
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourInclude.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import { Home, Bus, Ticket, Utensils, Users } from 'lucide-react'
+import { JSX } from 'react'
+
+const iconMap: Record<string, JSX.Element> = {
+  ticket: <Ticket className="text-blue-500" />,
+  utensils: <Utensils className="text-blue-500" />,
+  home: <Home className="text-blue-500" />,
+  bus: <Bus className="text-blue-500" />,
+  users: <Users className="text-blue-500" />,
+}
+
+export const TourInclude = ({ includes }: { includes: Include[] }) => {
+  return (
+    <div>
+      <h3 className="text-base font-semibold mb-2">Sudah Termasuk</h3>
+      <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
+        {includes.map((item) => (
+          <div key={item.id} className="flex items-center gap-2">
+            {iconMap[item.icon] || <span className="w-4" />}
+            <span>{item.text}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourList.tsx b/src/modules/DetailTourModule/module-elements/TourList.tsx
new file mode 100644
index 00000000..8a463cc1
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourList.tsx
@@ -0,0 +1,14 @@
+'use client'
+import { TourDay } from './TourDay'
+
+export const TourList = ({ section }: { section: Section[] }) => {
+  if (!section || section.length === 0) return null
+
+  return (
+    <div className="justify-center p-4">
+      {section.map((day) => (
+        <TourDay key={day.id} section={day} />
+      ))}
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/module-elements/TourOrderCard.tsx b/src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
new file mode 100644
index 00000000..36cf6a4d
--- /dev/null
+++ b/src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
@@ -0,0 +1,19 @@
+'use client'
+
+export const TourOrderCard = ({
+  pricePerTicket,
+}: {
+  pricePerTicket: number
+}) => {
+  return (
+    <div className="sticky top-4 w-full lg:w-72 bg-white p-4 rounded-xl shadow-md self-start">
+      <p className="text-xl font-semibold text-gray-800 text-raleway">
+        Rp{pricePerTicket.toLocaleString('id-ID')}
+        <span className="text-sm"> / pax</span>
+      </p>
+      <button className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg">
+        Pesan Sekarang
+      </button>
+    </div>
+  )
+}
diff --git a/src/modules/DetailTourModule/page.tsx b/src/modules/DetailTourModule/page.tsx
deleted file mode 100644
index efeef0c7..00000000
--- a/src/modules/DetailTourModule/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function DetailTourModule() {
-  return <></>
-}
diff --git a/src/modules/DetailTourModule/type.ts b/src/modules/DetailTourModule/type.ts
new file mode 100644
index 00000000..bd09422e
--- /dev/null
+++ b/src/modules/DetailTourModule/type.ts
@@ -0,0 +1,100 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
+
+interface Section {
+  updatedAt: string
+  createdAt: string
+  id: string
+  itineraryId: string
+  sectionNumber: number
+  contingencyPlanId: string | null
+  title: string
+  blocks: Block[]
+}
+
+interface Itinerary {
+  updatedAt: string
+  createdAt: string
+  id: string
+  userId: string
+  title: string
+  description: string
+  coverImage: string
+  startDate: string
+  endDate: string
+  tags: {
+    tag: Tag
+  }[]
+  isPublished: boolean
+  isCompleted: boolean
+  sections: Section[]
+  pendingInvites: {
+    createdAt: string // ISO string format (e.g., "2025-03-31T09:07:34.592Z")
+    email: string
+    id: string
+    itineraryId: string
+    updatedAt: string
+  }[]
+  invitedUsers: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string
+    email: string
+  }[]
+  user: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string | null
+  }
+  _count: {
+    likes: number
+  }
+}
+
+interface Tour {
+  id: string
+  title: string
+  coverImage: string
+  maxCapacity: number
+  description: string
+  location: string
+  pricePerTicket: number
+  duration: number
+  durationType: 'DAY' | 'HOUR'
+  itineraryId: string
+  createdAt: Date
+  updatedAt: Date
+  itinerary: Itinerary
+  includes: Include[]
+}
+
+interface Include {
+  id: number
+  tourid: number
+  icon: string
+  text: string
+}
+
+interface TourDetailResponse {
+  statusCode: number
+  success: boolean
+  message: string
+  data: Tour
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 15**

<details>
<summary>🧹 Nitpick comments (17)</summary><blockquote>

<details>
<summary>__tests__/TourOrderCard.test.tsx (1)</summary><blockquote>

`1-15`: **Enhance test coverage for the TourOrderCard component**

The tests correctly verify the basic rendering of price and button, but there are opportunities to improve test coverage:

1. Verify that different price values are displayed correctly
2. Check formatting of large numbers (e.g., thousands separators)
3. Test button click behavior if applicable


Consider adding these additional test cases:

```tsx
it('handles different price values correctly', () => {
  const { rerender } = render(<TourOrderCard pricePerTicket={0} />)
  expect(screen.getByText('Rp0')).toBeInTheDocument()
  
  rerender(<TourOrderCard pricePerTicket={1000000} />)
  expect(screen.getByText('Rp1000000')).toBeInTheDocument()
})

// If the button has click behavior
it('handles button click correctly', () => {
  const mockOnClick = jest.fn()
  render(<TourOrderCard pricePerTicket={250000} onOrderClick={mockOnClick} />)
  
  const button = screen.getByRole('button', { name: /Pesan Sekarang/i })
  button.click()
  
  expect(mockOnClick).toHaveBeenCalledTimes(1)
})
```

</blockquote></details>
<details>
<summary>__tests__/TourList.test.tsx (2)</summary><blockquote>

`12-60`: **Consider simplifying the mock data**

The mock data is very detailed and includes many properties that may not be necessary for testing the `TourList` component. Since your mock `TourDay` component only uses the `title` property, the test data could be simplified.

```diff
const mockSections = [
  {
    id: 'section1',
-    itineraryId: 'ITN-001',
-    sectionNumber: 1,
    title: 'Arrival Day',
-    updatedAt: '2022-04-03T10:00:00Z',
-    createdAt: '2022-04-03T10:00:00Z',
-    contingencyPlanId: null,
-    blocks: [
-      {
-        id: 'block1',
-        title: 'Airport',
-        description: 'Arrival at the airport',
-        updatedAt: '2022-04-03T10:00:00Z',
-        createdAt: '2022-04-03T10:00:00Z',
-        sectionId: 'section1',
-        position: 1,
-        blockType: 'LOCATION',
-        startTime: '2022-04-03T10:00:00Z',
-        endTime: '2022-04-03T12:00:00Z',
-        location: '0,0',
-        price: 0,
-        photoUrl: '',
-        routeToNext: {
-          id: 'route1',
-          createdAt: '2022-04-03T12:00:00Z',
-          updatedAt: '2022-04-03T12:00:00Z',
-          sourceBlockId: 'block1',
-          destinationBlockId: 'block2',
-          distance: 1000,
-          duration: 600,
-          polyline: '',
-          transportMode: TransportMode.WALK,
-        },
-      },
-    ],
  },
  {
    id: 'section2',
-    itineraryId: 'ITN-001',
-    sectionNumber: 2,
    title: 'Tour Day',
-    updatedAt: '2022-04-04T10:00:00Z',
-    createdAt: '2022-04-04T10:00:00Z',
-    contingencyPlanId: null,
-    blocks: [],
  },
]
```

---

`62-75`: **Add test for undefined section prop**

Your current tests cover empty array and populated array cases, but don't test what happens if the section prop is undefined. Based on the component implementation, it should handle this case but it's not tested.

```diff
describe('TourList', () => {
  it('renders nothing when section is empty', () => {
    const { container } = render(<TourList section={[]} />)
    expect(container.firstChild).toBeNull()
  })

+ it('renders nothing when section is undefined', () => {
+   const { container } = render(<TourList section={undefined} />)
+   expect(container.firstChild).toBeNull()
+ })

  it('renders TourDay components for each section', () => {
    render(<TourList section={mockSections} />)
    const tourDays = screen.getAllByTestId('tour-day')
    expect(tourDays).toHaveLength(2)
    expect(tourDays[0]).toHaveTextContent('Arrival Day')
    expect(tourDays[1]).toHaveTextContent('Tour Day')
  })
})
```

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourOrderCard.tsx (1)</summary><blockquote>

`3-7`: **Consider using TypeScript interface for props**

For better code organization and reusability, it's a good practice to define interfaces for component props.


Define an interface for the component props:

```diff
+interface TourOrderCardProps {
+  pricePerTicket: number;
+}
+
-export const TourOrderCard = ({
-  pricePerTicket,
-}: {
-  pricePerTicket: number
-}) => {
+export const TourOrderCard = ({ pricePerTicket }: TourOrderCardProps) => {
```

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourList.tsx (1)</summary><blockquote>

`4-14`: **Consider using TypeScript interface for props**

For better code organization, it's good practice to define interfaces for component props.


Define an interface for the component props:

```diff
+import { Section } from '../type'
+
+interface TourListProps {
+  section: Section[];
+}
+
-export const TourList = ({ section }: { section: Section[] }) => {
+export const TourList = ({ section }: TourListProps) => {
```

</blockquote></details>
<details>
<summary>__tests__/TourHeader.test.tsx (1)</summary><blockquote>

`37-53`: **Improve type handling in the test case**

In the test case for the gradient background, `coverImage: undefined` is used but then converted to an empty string. This doesn't accurately test the component's behavior with undefined values.


Modify the test to be more direct:

```diff
  it('applies gradient background when coverImage is not provided', () => {
    const mockTourNoImage = {
      ...mockTour,
-      coverImage: undefined,
+      coverImage: '',
    }
    const { container } = render(
      <TourHeader
        title={mockTourNoImage.title}
        location={mockTourNoImage.location}
-        coverImage={mockTourNoImage.coverImage || ''}
+        coverImage={mockTourNoImage.coverImage}
      />
    )
    const backgroundDiv = container.querySelector('div.relative')
    expect(backgroundDiv).toHaveStyle(
      'background-image: linear-gradient(360deg, #004080, #0073E6, #60A5FA)'
    )
  })
```

</blockquote></details>
<details>
<summary>__tests__/TourDay.test.tsx (1)</summary><blockquote>

`10-61`: **Consider using Jest's beforeEach for the mock data setup**

The mock data is quite extensive and could be set up once for all tests to improve test readability.


Consider moving the mock data setup to a beforeEach block:

```diff
-const mockSection = {
+let mockSection;
+
+beforeEach(() => {
+  mockSection = {
   id: 'section1',
   // ... rest of the mock data
   ],
-}
+  }
+})
```

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourDescription.tsx (2)</summary><blockquote>

`16-16`: **Consider memoizing the shortened description calculation**

The shortened description is recalculated on every render, which could be inefficient for very long descriptions.


```diff
- const shortDesc = description.split(' ').slice(0, 30).join(' ') + '...'
+ const shortDesc = useMemo(() => {
+   return description.split(' ').slice(0, 30).join(' ') + '...'
+ }, [description])
```

Don't forget to import `useMemo` from React.

---

`6-14`: **Add a proper TypeScript interface for component props**

Using an inline type definition works, but a named interface would improve code readability and reusability.


```diff
+interface TourDescriptionProps {
+  title: string
+  description: string
+  maxCapacity: number
+}

-export const TourDescription = ({
-  title,
-  description,
-  maxCapacity,
-}: {
-  title: string
-  description: string
-  maxCapacity: number
-}) => {
+export const TourDescription = ({
+  title,
+  description,
+  maxCapacity,
+}: TourDescriptionProps) => {
```

</blockquote></details>
<details>
<summary>__tests__/TourDescription.test.tsx (3)</summary><blockquote>

`9-9`: **Remove leading space in the test title**

There's an extra space at the beginning of the title in the mockProps, but the test is looking for the trimmed version.


```diff
-  title: ' Amazing Bali Tour',
+  title: 'Amazing Bali Tour',
```

---

`4-6`: **Improve the mock implementation with test props**

The current mock doesn't ensure the icon receives and correctly handles its props. Consider a more realistic mock implementation.


```diff
jest.mock('lucide-react', () => ({
-  UsersRound: () => <div data-testid="users-icon" />,
+  UsersRound: (props) => <div data-testid="users-icon" {...props} />,
}))
```

---

`1-52`: **Add edge case tests for description handling**

Consider adding tests for edge cases such as empty descriptions or very long single-word descriptions.


```typescript
it('handles empty description gracefully', () => {
  const emptyProps = { ...mockProps, description: '' };
  render(<TourDescription {...emptyProps} />);
  
  // Verify it doesn't crash and shows appropriate fallback
  expect(screen.getByRole('button')).toBeInTheDocument();
});
```

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/index.tsx (1)</summary><blockquote>

`52-53`: **Add TODO comments with explanations for commented components**

The commented components lack context about why they're commented out or when they'll be implemented.


```diff
-          {/* <TourInclude includes={[]} /> */}
-          {/* <TourSchedule/> */}
+          {/* TODO: Implement TourInclude component when services/includes data is available */}
+          {/* TODO: Implement TourSchedule component when schedule data model is finalized */}
```

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourDay.tsx (4)</summary><blockquote>

`8-10`: **Use string literal instead of template literal for static classes**  
You're using a template string for a purely static `className`. Convert to a normal string to improve readability:  
```diff
- className={`absolute -left-5 top-5 bg-[#024C98] text-white font-bold w-8 h-12 flex items-center justify-center`}
+ className="absolute -left-5 top-5 bg-[#024C98] text-white font-bold w-8 h-12 flex items-center justify-center"
```

---

`20-29`: **Extract `formatTime` outside the loop**  
Defining `formatTime` inside each iteration creates a new function on every render. Move it above the `return` in the component (or into a shared util) to avoid unnecessary redeclarations and allow unit-testing it in isolation.

---

`6-7`: **Avoid inline hex codes in class names**  
Consider using Tailwind’s theme colors (e.g. `bg-gray-400`, `bg-blue-800`) or extracting these hex values into your Tailwind config for consistency and maintainability.

---

`11-16`: **Add ARIA attributes or hide decorative icons**  
The `MapPin` and `Clock` icons are purely decorative. Either add `aria-hidden="true"` or supply an appropriate `aria-label` to improve accessibility.  



Also applies to: 44-49

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 38f8de10897d57fe8b96c443480f836d63c53222 and ee695e7f70320b992a2f37ec407e50da0d06c349.

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `__tests__/TourDay.test.tsx` (1 hunks)
* `__tests__/TourDescription.test.tsx` (1 hunks)
* `__tests__/TourHeader.test.tsx` (1 hunks)
* `__tests__/TourList.test.tsx` (1 hunks)
* `__tests__/TourOrderCard.test.tsx` (1 hunks)
* `src/app/(main)/tour/[id]/page.tsx` (1 hunks)
* `src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx` (2 hunks)
* `src/modules/DetailTourModule/index.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourDay.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourDescription.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourHeader.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourList.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourOrderCard.tsx` (1 hunks)
* `src/modules/DetailTourModule/type.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (8)</summary>

<details>
<summary>__tests__/TourList.test.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourList.tsx (1)</summary>

* `TourList` (4-14)

</details>

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/index.tsx (4)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourHeader.tsx (1)</summary>

* `TourHeader` (1-26)

</details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourDescription.tsx (1)</summary>

* `TourDescription` (6-39)

</details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourOrderCard.tsx (1)</summary>

* `TourOrderCard` (3-19)

</details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourList.tsx (1)</summary>

* `TourList` (4-14)

</details>

</blockquote></details>
<details>
<summary>__tests__/TourOrderCard.test.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourOrderCard.tsx (1)</summary>

* `TourOrderCard` (3-19)

</details>

</blockquote></details>
<details>
<summary>__tests__/TourDescription.test.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourDescription.tsx (1)</summary>

* `TourDescription` (6-39)

</details>

</blockquote></details>
<details>
<summary>__tests__/TourDay.test.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourDay.tsx (1)</summary>

* `TourDay` (4-66)

</details>

</blockquote></details>
<details>
<summary>__tests__/TourHeader.test.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourHeader.tsx (1)</summary>

* `TourHeader` (1-26)

</details>

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourDay.tsx (2)</summary><blockquote>

<details>
<summary>src/modules/ItineraryMakerModule/interface.ts (1)</summary>

* `Section` (26-30)

</details>
<details>
<summary>src/lib/utils.ts (1)</summary>

* `cn` (4-6)

</details>

</blockquote></details>
<details>
<summary>src/modules/DetailTourModule/type.ts (1)</summary><blockquote>

<details>
<summary>src/modules/ItineraryMakerModule/interface.ts (3)</summary>

* `Block` (4-15)
* `Route` (17-24)
* `Section` (26-30)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: SonarQube Scan</summary>

<details>
<summary>__tests__/TourDay.test.tsx</summary>

[error] 79-79: TestingLibraryElementError: Unable to find an element with the text: /17.00 - 19.00/. The text might be broken up by multiple elements, making the matcher fail.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>__tests__/TourList.test.tsx (1)</summary>

`6-10`: **Mock implementation is well-structured**

The mock implementation of `TourDay` is appropriately simplified and focused on what's needed for testing. It correctly renders the section title which is then verified in the tests.

</details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourList.tsx (1)</summary>

`5-5`: **LGTM: Good null/empty check**

The component correctly handles empty or undefined section data by returning null.

</details>
<details>
<summary>__tests__/TourHeader.test.tsx (1)</summary>

`1-54`: **LGTM: Well-structured test suite**

The test suite is well-organized and covers the key functionality of the TourHeader component, including text rendering and conditional background image handling.

</details>
<details>
<summary>__tests__/TourDay.test.tsx (2)</summary>

`5-8`: **LGTM: Good mocking approach**

The approach to mock the `lucide-react` icons is clean and effective, making the tests more focused on the component's behavior rather than external dependencies.

---

`79-79`: <details>
<summary>❌ Incorrect review comment</summary>

**Fix failing test for time format**

There's a pipeline failure related to the time format in this test. The component is likely rendering the time in a different format than what's expected in the test.


The test is expecting "17.00 - 19.00" but the component might be using a different format. Check the `formatTime` function in the `TourDay` component and update the test to match the actual output:

```diff
-    expect(screen.getByText(/17.00 - 19.00/)).toBeInTheDocument()
+    // Based on the time in mockSection (10:00-12:00) and Indonesian locale
+    expect(screen.getByText(/10:00 - 12:00/)).toBeInTheDocument()
```

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the actual time format used in the TourDay component
cat "src/modules/DetailTourModule/module-elements/TourDay.tsx" | grep -A 15 "formatTime"
```

Length of output: 1233

---

🏁 Script executed:

```shell
#!/bin/bash
sed -n '1,200p' __tests__/TourDay.test.tsx
```

Length of output: 2657

---



</details>

**Ignore incorrect time format suggestion**  
The `formatTime` helper uses `toLocaleTimeString('id-ID')` with the test environment’s timezone, converting `2022-04-03T10:00:00Z` → `17.00` and `12:00Z` → `19.00`. The existing assertion:  
```ts
expect(screen.getByText(/17.00 - 19.00/)).toBeInTheDocument()
```  
is therefore correct. No changes are required. 

> Likely an incorrect or invalid review comment.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: SonarQube Scan</summary>

[error] 79-79: TestingLibraryElementError: Unable to find an element with the text: /17.00 - 19.00/. The text might be broken up by multiple elements, making the matcher fail.

</details>

</details>

</details>
<details>
<summary>src/modules/DetailTourModule/module-elements/TourHeader.tsx (1)</summary>

`1-26`: **LGTM! Clean and straightforward component implementation.**

The component properly displays tour title, location, and cover image with a nice fallback gradient if no image is provided. The responsive design with different heights for mobile and desktop is a good touch.

For future enhancements, consider:
1. Adding an interface for the props instead of inline type definition
2. Adding accessibility attributes for the background image container
3. Handling image loading errors

</details>
<details>
<summary>__tests__/TourDescription.test.tsx (4)</summary>

`14-20`: **LGTM! Test covers basic component rendering**

This test correctly verifies that the component renders the title, icon, and capacity text.

---

`22-28`: **LGTM! Test verifies default description state**

This test correctly checks that the description is truncated by default and the "show more" button is present.

---

`30-38`: **LGTM! Test verifies expand description functionality**

This test correctly simulates a button click and verifies that the full description and "hide" button are displayed.

---

`40-51`: **LGTM! Test verifies collapse description functionality**

This test correctly verifies the toggle back to the shortened description after clicking twice.

</details>
<details>
<summary>src/modules/DetailTourModule/index.tsx (1)</summary>

`58-58`: <details>
<summary>✅ Verification successful</summary>

**Handle empty sections gracefully**

Line 58 passes `data.itinerary.sections` to TourList without checking if it exists or is empty.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if TourList component handles empty or undefined sections

# First, let's find the component definition to confirm its behavior
fd "TourList" --type file | xargs cat | grep -A 15 "TourList"
```

Length of output: 1382

---



</details>

**TourList already handles empty or undefined sections**

`TourList` checks for a falsy or empty `section` prop and returns `null`, so you can safely pass `data.itinerary.sections` without an additional guard.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T10:32:36Z
user: login: daffarafi
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T18:36:19Z
user: login: fahmi-ramadhan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T05:19:09Z
user: login: fahmi-ramadhan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T05:22:01Z
user: login: matthewhjt
body: Halo, saya melihat masih ada beberapa kesalahan seperti import komponen yang tidak ada, dan kode yang masih dikomen. Selain itu, ada juga merge conflict yang harus disolve terlebih dahulu. Terima kasih.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:31:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>♻️ Duplicate comments (4)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/type.ts (4)</summary><blockquote>

`1-101`: _⚠️ Potential issue_

**Export all interfaces**

None of the interfaces are exported, which means they can't be imported or used outside this file. Add the `export` keyword to each interface.


```diff
-interface Block {
+export interface Block {
  // ...
}

-interface Section {
+export interface Section {
  // ...
}

-interface Itinerary {
+export interface Itinerary {
  // ...
}

-interface Tour {
+export interface Tour {
  // ...
}

-interface Include {
+export interface Include {
  // ...
}

-interface TourDetailResponse {
+export interface TourDetailResponse {
  // ...
}
```

---

`40-42`: _⚠️ Potential issue_

**Missing definition or import of Tag**

The `Itinerary` interface references `Tag` but there's no definition or import for it.


```diff
+interface Tag {
+  id: string
+  name: string
+}
+
interface Itinerary {
  // ...
  tags: {
    tag: Tag
  }[]
  // ...
}
```

---

`82-83`: _⚠️ Potential issue_

**Use string for createdAt/updatedAt instead of Date**

All other timestamps in your types are defined as strings, but these are typed as Date. This mismatch could cause deserialization issues.


```diff
-  createdAt: Date
-  updatedAt: Date
+  createdAt: string
+  updatedAt: string
```

---

`90-91`: _🛠️ Refactor suggestion_

**Rename tourid to camelCase tourId**

For consistency with TypeScript naming conventions, property names should use camelCase.


```diff
-  tourid: number
+  tourId: number
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/modules/DetailTourModule/module-elements/TourInclude.tsx (2)</summary><blockquote>

`6-12`: **Consider adding accessibility attributes to icons**

The icons currently lack accessibility attributes. Add aria-label or aria-hidden attributes to improve accessibility.

```diff
const iconMap: Record<string, JSX.Element> = {
-  ticket: <Ticket className="text-blue-500" />,
-  utensils: <Utensils className="text-blue-500" />,
-  home: <Home className="text-blue-500" />,
-  bus: <Bus className="text-blue-500" />,
-  users: <Users className="text-blue-500" />,
+  ticket: <Ticket className="text-blue-500" aria-hidden="true" />,
+  utensils: <Utensils className="text-blue-500" aria-hidden="true" />,
+  home: <Home className="text-blue-500" aria-hidden="true" />,
+  bus: <Bus className="text-blue-500" aria-hidden="true" />,
+  users: <Users className="text-blue-500" aria-hidden="true" />,
}
```

---

`14-28`: **Add empty state handling**

The component doesn't handle the case where the `includes` array is empty. Consider adding a conditional to display a message or hide the section when there are no included items.

```diff
export const TourInclude = ({ includes }: { includes: Include[] }) => {
+  if (includes.length === 0) {
+    return null;
+  }
+
  return (
    <div>
      <h3 className="text-base font-semibold mb-2">Sudah Termasuk</h3>
      <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
        {includes.map((item) => (
          <div key={item.id} className="flex items-center gap-2">
            {iconMap[item.icon] || <span className="w-4" />}
            <span>{item.text}</span>
          </div>
        ))}
      </div>
    </div>
  )
}
```

</blockquote></details>
<details>
<summary>src/app/(main)/tour/[id]/page.tsx (1)</summary><blockquote>

`1-23`: **Add error logging**

The catch block swallows all errors without logging them. Consider adding error logging to help with debugging.

```diff
try {
  const res = await customFetch<TourDetailResponse>(
    `/tour/${params.id}`
  )

  if (res.statusCode === 404 || res.statusCode === 403) {
    return notFound()
  }

  return <DetailTourModule initialData={res.data} />
} catch (err) {
+  console.error('Error fetching tour details:', err)
  return notFound()
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0da8547f6aa312ad39ea2e95f4534fdf417481f4 and ce1c2054d70bf453b3d399d4a8d11909e633f6f7.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `src/app/(main)/tour/[id]/page.tsx` (1 hunks)
* `src/modules/DetailTourModule/index.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourInclude.tsx` (1 hunks)
* `src/modules/DetailTourModule/module-elements/TourOrderCard.tsx` (1 hunks)
* `src/modules/DetailTourModule/page.tsx` (0 hunks)
* `src/modules/DetailTourModule/type.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* src/modules/DetailTourModule/page.tsx

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/modules/DetailTourModule/index.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/modules/DetailTourModule/module-elements/TourOrderCard.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/modules/DetailTourModule/type.ts (1)</summary><blockquote>

<details>
<summary>src/modules/ItineraryMakerModule/interface.ts (3)</summary>

* `Block` (4-15)
* `Route` (17-24)
* `Section` (26-30)

</details>

</blockquote></details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T15:06:28Z

Review comments:
diff_hunk: @@ -0,0 +1,5 @@
+import DetailTourModule from '@/modules/DetailTourModule'
+
+export default function TourDetailPage() {
+  return <DetailTourModule />
+}
path: src/app/(main)/tour/[id]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Pass the tour ID parameter to the DetailTourModule**

The component correctly follows Next.js App Router conventions, but it doesn't pass the dynamic route parameter (`[id]`) to the `DetailTourModule` component. If `DetailTourModule` needs to know which tour to fetch and display, it would need this ID.

```diff
import DetailTourModule from '@/modules/DetailTourModule'

export default function TourDetailPage() {
-  return <DetailTourModule />
+  return <DetailTourModule />
}
```

Consider modifying the implementation to pass the tour ID parameter:

```diff
import DetailTourModule from '@/modules/DetailTourModule'
+ import { notFound } from 'next/navigation'

- export default function TourDetailPage() {
+ export default function TourDetailPage({ params }: { params: { id: string } }) {
+   if (!params.id) return notFound()
-   return <DetailTourModule />
+   return <DetailTourModule tourId={params.id} />
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import DetailTourModule from '@/modules/DetailTourModule'
import { notFound } from 'next/navigation'

export default function TourDetailPage({ params }: { params: { id: string } }) {
  if (!params.id) return notFound()
  return <DetailTourModule tourId={params.id} />
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 327e2b8 to ce1c205
created_at: 2025-05-09T10:32:34Z
author_association: NONE
diff_hunk: @@ -15,6 +15,7 @@ import { customFetch } from '@/utils/newCustomFetch'
 import { toast } from 'sonner'
 import { DuplicateItineraryResponse } from '@/modules/ItineraryModule/module-elements/types'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import Head from 'next/head'
path: src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider alternative approaches for Open Graph metadata**

While adding Open Graph tags via the `Head` component from `next/head` works for social sharing, there are a few potential issues:

1. In Next.js App Router, `next/head` is deprecated in favor of the Metadata API
2. Social media crawlers typically don't execute JavaScript, so showing these tags only when a dialog is open might not work for social sharing
3. Metadata should be set at the page level for optimal social media preview generation


Consider using Next.js 13+ Metadata API instead of the `Head` component, and place the metadata at the page level rather than conditionally in a dialog:

```tsx
// In your page.tsx file
import type { Metadata } from 'next'

export async function generateMetadata({ params }): Promise<Metadata> {
  // Fetch your itinerary data here
  const data = await fetchItinerary(params.id)
  
  return {
    openGraph: {
      title: data.title,
      description: data.description || 'Lihat itinerary ini!',
      images: [
        {
          url: data.coverImage || '/default-image.jpg',
          width: 1200,
          height: 630,
        },
      ],
    },
  }
}
```


Also applies to: 347-353

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+'use client'
+
+export const TourOrderCard = ({
+  pricePerTicket,
+}: {
+  pricePerTicket: number
+}) => {
+  return (
+    <div className="sticky top-4 w-full lg:w-72 bg-white p-4 rounded-xl shadow-md self-start">
+      <p className="text-xl font-semibold text-gray-800 text-raleway">
+        Rp{pricePerTicket}
+        <span className="text-sm"> / pax</span>
+      </p>
+      <button className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg">
+        Pesan Sekarang
+      </button>
path: src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add button interaction and accessibility attributes**

The button currently lacks accessibility attributes and doesn't have any click handler.


Enhance the button with accessibility attributes and a click handler:

```diff
-      <button className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg">
+      <button 
+        className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg hover:opacity-90 focus:ring-2 focus:ring-blue-300 transition-opacity"
+        onClick={() => {/* Implement booking logic here */}}
+        aria-label="Book this tour now"
+      >
         Pesan Sekarang
       </button>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      <button 
        className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg hover:opacity-90 focus:ring-2 focus:ring-blue-300 transition-opacity"
        onClick={() => {/* Implement booking logic here */}}
        aria-label="Book this tour now"
      >
        Pesan Sekarang
      </button>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+'use client'
+
+export const TourOrderCard = ({
+  pricePerTicket,
+}: {
+  pricePerTicket: number
+}) => {
+  return (
+    <div className="sticky top-4 w-full lg:w-72 bg-white p-4 rounded-xl shadow-md self-start">
+      <p className="text-xl font-semibold text-gray-800 text-raleway">
+        Rp{pricePerTicket}
+        <span className="text-sm"> / pax</span>
path: src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Format the price for better readability**

The price is displayed without any thousand separators, which can make it difficult to read for larger amounts.


Consider formatting the price with thousand separators:

```diff
-        Rp{pricePerTicket}
+        Rp{pricePerTicket.toLocaleString('id-ID')}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      <p className="text-xl font-semibold text-gray-800 text-raleway">
        Rp{pricePerTicket.toLocaleString('id-ID')}
        <span className="text-sm"> / pax</span>
      </p>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 327e2b8 to ce1c205
created_at: 2025-05-09T10:32:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,14 @@
+'use client'
+import { TourDay } from './TourDay'
+
+export const TourList = ({ section }: { section: Section[] }) => {
path: src/modules/DetailTourModule/module-elements/TourList.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing import for the `Section` type**

The component uses the `Section` type but doesn't import it.


Add the missing import:

```diff
+import { Section } from '../type'
 export const TourList = ({ section }: { section: Section[] }) => {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
+ import { Section } from '../type'

  export const TourList = ({ section }: { section: Section[] }) => {
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:34Z
author_association: NONE
diff_hunk: @@ -0,0 +1,39 @@
+'use client'
+
+import { useState } from 'react'
+import { UsersRound } from 'lucide-react'
+
+export const TourDescription = ({
+  title,
+  description,
+  maxCapacity,
+}: {
+  title: string
+  description: string
+  maxCapacity: number
+}) => {
+  const [expanded, setExpanded] = useState(false)
+  const shortDesc = description.split(' ').slice(0, 30).join(' ') + '...'
+
+  return (
+    <div className="text-sm text-gray-700 space-y-2">
+      <h2 className="text-base font-semibold">{title}</h2>
+
+      <div className="flex items-center gap-2 text-gray-600 text-sm">
+        <UsersRound className="text-gray-500" />
+        <span>Open trip sampai {maxCapacity} orang</span>
+      </div>
+
+      <p className="whitespace-pre-line">
+        {expanded ? description : shortDesc}
+      </p>
path: src/modules/DetailTourModule/module-elements/TourDescription.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for missing or empty description**

The current implementation assumes description is always a valid string. Consider handling edge cases to prevent potential runtime errors.


```diff
<p className="whitespace-pre-line">
-  {expanded ? description : shortDesc}
+  {!description ? 'No description available' : expanded ? description : shortDesc}
</p>
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
      <p className="whitespace-pre-line">
        {!description ? 'No description available' : expanded ? description : shortDesc}
      </p>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
+
+  const fetchData = async () => {
+    try {
+      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
+        credentials: 'include',
+      })
+
+      if (res.statusCode === 404 || res.statusCode === 403) {
+        setIsNotFound(true)
+      }
+
+      setData(res.data)
+    } catch (err: any) {
+      setIsNotFound(true)
+    }
path: src/modules/DetailTourModule/index.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle not found state in the UI**

You're setting `isNotFound` state but not using it in the render method. The UI should display a "not found" message when appropriate.


```diff
  return data ? (
    <div className="max-w-6xl mx-auto p-4 pt-24 py-8">
      {/* ... existing code ... */}
    </div>
+  ) : isNotFound ? (
+    <div className="flex flex-col items-center justify-center h-screen">
+      <h2 className="text-xl font-semibold mb-2">Tour Not Found</h2>
+      <p className="text-gray-600">The requested tour could not be found.</p>
+    </div>
  ) : (
    <div className="flex items-center justify-center h-screen">
      <Loader2 className="animate-spin w-6 h-6 mr-2" />
    </div>
  )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
return data ? (
  <div className="max-w-6xl mx-auto p-4 pt-24 py-8">
    {/* ... existing code ... */}
  </div>
) : isNotFound ? (
  <div className="flex flex-col items-center justify-center h-screen">
    <h2 className="text-xl font-semibold mb-2">Tour Not Found</h2>
    <p className="text-gray-600">The requested tour could not be found.</p>
  </div>
) : (
  <div className="flex items-center justify-center h-screen">
    <Loader2 className="animate-spin w-6 h-6 mr-2" />
  </div>
);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
+
+  const fetchData = async () => {
+    try {
+      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
+        credentials: 'include',
+      })
+
+      if (res.statusCode === 404 || res.statusCode === 403) {
+        setIsNotFound(true)
+      }
+
+      setData(res.data)
+    } catch (err: any) {
+      setIsNotFound(true)
+    }
+  }
+
+  useEffect(() => {
+    void fetchData()
+  }, [id])
path: src/modules/DetailTourModule/index.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix useEffect dependencies and data fetching pattern**

The current pattern doesn't properly include dependencies, which could lead to stale closures.


```diff
- useEffect(() => {
-   void fetchData()
- }, [id])
+ useEffect(() => {
+   fetchData()
+ }, [id, fetchData])

+ // Memoize fetchData to prevent dependency changes
+ const fetchData = useCallback(async () => {
+   // existing implementation
+ }, [id])
```

Don't forget to import `useCallback` from React.


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
path: src/modules/DetailTourModule/index.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add missing type imports**

The component uses Tour and TourDetailResponse types but doesn't import them. Add the necessary imports.


```diff
import { TourList } from './module-elements/TourList'
+import { Tour, TourDetailResponse } from './type'

export default function DetailTourModule() {
  const [data, setData] = useState<Tour | null>(null)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { TourList } from './module-elements/TourList'
+import { Tour, TourDetailResponse } from './type'

export default function DetailTourModule() {
  const [data, setData] = useState<Tour | null>(null)
  const [isNotFound, setIsNotFound] = useState(false)
  const { id } = useParams<{ id: string }>()
  // …
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
+
+  const fetchData = async () => {
+    try {
+      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
+        credentials: 'include',
+      })
+
+      if (res.statusCode === 404 || res.statusCode === 403) {
+        setIsNotFound(true)
+      }
+
+      setData(res.data)
+    } catch (err: any) {
+      setIsNotFound(true)
+    }
+  }
path: src/modules/DetailTourModule/index.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling in fetchData**

The current error handling is basic and doesn't provide detailed error information. Also, you're using `: any` type for errors.


```diff
  const fetchData = async () => {
    try {
      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
        credentials: 'include',
      })

      if (res.statusCode === 404 || res.statusCode === 403) {
        setIsNotFound(true)
+       return
      }

      setData(res.data)
-   } catch (err: any) {
+   } catch (err) {
+     console.error('Failed to fetch tour data:', err)
      setIsNotFound(true)
    }
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const fetchData = async () => {
    try {
      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
        credentials: 'include',
      })

      if (res.statusCode === 404 || res.statusCode === 403) {
        setIsNotFound(true)
        return
      }

      setData(res.data)
    } catch (err) {
      console.error('Failed to fetch tour data:', err)
      setIsNotFound(true)
    }
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,66 @@
+import { cn } from '@/lib/utils'
+import { Clock, MapPin } from 'lucide-react'
+
+export const TourDay = ({ section }: { section: Section }) => {
path: src/modules/DetailTourModule/module-elements/TourDay.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing import of `Section` type**  
The component signature uses `Section` but it isn’t imported. TypeScript will error. Add an import such as:  
```ts
import { Section } from '@/modules/DetailTourModule/type'
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,99 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
+
+interface Section {
+  updatedAt: string
+  createdAt: string
+  id: string
+  itineraryId: string
+  sectionNumber: number
+  contingencyPlanId: string | null
+  title: string
+  blocks: Block[]
+}
+
+interface Itinerary {
+  updatedAt: string
+  createdAt: string
+  id: string
+  userId: string
+  title: string
+  description: string
+  coverImage: string
+  startDate: string
+  endDate: string
+  tags: {
+    tag: Tag
+  }[]
+  isPublished: boolean
+  isCompleted: boolean
+  sections: Section[]
+  pendingInvites: {
+    createdAt: string // ISO string format (e.g., "2025-03-31T09:07:34.592Z")
+    email: string
+    id: string
+    itineraryId: string
+    updatedAt: string
+  }[]
+  invitedUsers: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string
+    email: string
+  }[]
+  user: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string | null
+  }
+  _count: {
+    likes: number
+  }
+}
+
+interface Tour {
+  id: string
+  title: string
+  coverImage: string
+  maxCapacity: number
+  description: string
+  location: string
+  pricePerTicket: number
+  duration: number
+  durationType: 'DAY' | 'HOUR'
+  itineraryId: string
+  createdAt: Date
+  updatedAt: Date
+  itinerary: Itinerary
+}
+
+interface Include {
+  id: number
+  tourid: number
+  icon: string
+  text: string
+}
+
+interface TourDetailResponse {
+  statusCode: number
+  success: boolean
+  message: string
+  data: Tour
+}
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Export all interfaces**  
None of the `interface` declarations are exported. To be consumable across the module, prepend `export` to each (`Block`, `Section`, `Itinerary`, `Tour`, `Include`, `TourDetailResponse`). TypeScript will otherwise report “cannot find name” errors.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,99 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
+
+interface Section {
+  updatedAt: string
+  createdAt: string
+  id: string
+  itineraryId: string
+  sectionNumber: number
+  contingencyPlanId: string | null
+  title: string
+  blocks: Block[]
+}
+
+interface Itinerary {
+  updatedAt: string
+  createdAt: string
+  id: string
+  userId: string
+  title: string
+  description: string
+  coverImage: string
+  startDate: string
+  endDate: string
+  tags: {
+    tag: Tag
+  }[]
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing definition or import of `Tag`**  
The `Itinerary` type references `Tag` but there is no local declaration or import. Either define the `Tag` interface here or import it from where it is declared.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,99 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
+
+interface Section {
+  updatedAt: string
+  createdAt: string
+  id: string
+  itineraryId: string
+  sectionNumber: number
+  contingencyPlanId: string | null
+  title: string
+  blocks: Block[]
+}
+
+interface Itinerary {
+  updatedAt: string
+  createdAt: string
+  id: string
+  userId: string
+  title: string
+  description: string
+  coverImage: string
+  startDate: string
+  endDate: string
+  tags: {
+    tag: Tag
+  }[]
+  isPublished: boolean
+  isCompleted: boolean
+  sections: Section[]
+  pendingInvites: {
+    createdAt: string // ISO string format (e.g., "2025-03-31T09:07:34.592Z")
+    email: string
+    id: string
+    itineraryId: string
+    updatedAt: string
+  }[]
+  invitedUsers: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string
+    email: string
+  }[]
+  user: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string | null
+  }
+  _count: {
+    likes: number
+  }
+}
+
+interface Tour {
+  id: string
+  title: string
+  coverImage: string
+  maxCapacity: number
+  description: string
+  location: string
+  pricePerTicket: number
+  duration: number
+  durationType: 'DAY' | 'HOUR'
+  itineraryId: string
+  createdAt: Date
+  updatedAt: Date
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`createdAt`/`updatedAt` should be `string`, not `Date`**  
All other timestamps in your API response types are typed as `string`. Treating these as `Date` will misrepresent the raw JSON payload and cause deserialization issues. Change to:  
```ts
createdAt: string
updatedAt: string
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,99 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
+
+interface Section {
+  updatedAt: string
+  createdAt: string
+  id: string
+  itineraryId: string
+  sectionNumber: number
+  contingencyPlanId: string | null
+  title: string
+  blocks: Block[]
+}
+
+interface Itinerary {
+  updatedAt: string
+  createdAt: string
+  id: string
+  userId: string
+  title: string
+  description: string
+  coverImage: string
+  startDate: string
+  endDate: string
+  tags: {
+    tag: Tag
+  }[]
+  isPublished: boolean
+  isCompleted: boolean
+  sections: Section[]
+  pendingInvites: {
+    createdAt: string // ISO string format (e.g., "2025-03-31T09:07:34.592Z")
+    email: string
+    id: string
+    itineraryId: string
+    updatedAt: string
+  }[]
+  invitedUsers: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string
+    email: string
+  }[]
+  user: {
+    id: string
+    firstName: string
+    lastName: string
+    photoProfile: string | null
+  }
+  _count: {
+    likes: number
+  }
+}
+
+interface Tour {
+  id: string
+  title: string
+  coverImage: string
+  maxCapacity: number
+  description: string
+  location: string
+  pricePerTicket: number
+  duration: number
+  durationType: 'DAY' | 'HOUR'
+  itineraryId: string
+  createdAt: Date
+  updatedAt: Date
+  itinerary: Itinerary
+}
+
+interface Include {
+  id: number
+  tourid: number
+  icon: string
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Rename `tourid` to camelCase `tourId`**  
For consistency with the rest of the codebase and TypeScript conventions, rename the property to `tourId`.  
```diff
- tourid: number
+ tourId: number
```  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  tourId: number
  icon: string
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T10:32:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,5 @@
+import DetailTourModule from '@/modules/DetailTourModule'
+
+export default function TourDetailPage() {
+  return <DetailTourModule />
+}
path: src/app/(main)/tour/[id]/page.tsx
user: login: daffarafi
body: Halo Nasya! Saya melihat bahwa halaman detail tour saat ini menggunakan `useParams` di dalam komponen `DetailTourModule` untuk mendapatkan ID tour. Ada pendekatan alternatif yang mungkin bisa dipertimbangkan untuk pengembangan selanjutnya.

Daripada menggunakan `useParams` di dalam komponen `DetailTourModule`, kita bisa langsung meneruskan ID sebagai prop dari komponen halaman:

```typescript
import DetailTourModule from '@/modules/DetailTourModule'

export default function TourDetailPage({
  params,
}: Readonly<{
  params: Promise<{ id: string }>
}>) {
  const { id } = await params
  return <DetailTourModule id={id} />
}
```

Menurut saya cara ini dapat membuat kode lebih clean, dengan cara memanfaatkan fitur-fitur yang sudah disediakan oleh NextJS
created_at: 2025-05-09T18:24:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
+
+  const fetchData = async () => {
+    try {
+      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
+        credentials: 'include',
+      })
+
+      if (res.statusCode === 404 || res.statusCode === 403) {
+        setIsNotFound(true)
+      }
+
+      setData(res.data)
+    } catch (err: any) {
+      setIsNotFound(true)
+    }
path: src/modules/DetailTourModule/index.tsx
user: login: daffarafi
body: Selain itu, disini saya melihat kamu memanggil data tour secara _client-sided_ ya? Sebenarnya pendekatan seperti ini dapat digunakan. Tapi ada cara lain yang lebih baik dalam melakukan hal ini.

Berhubung endpoint yang digunakan merupakan endpoint Public, seharusnya endpoint ini bisa dipanggil dengan cara _server-sided_ pada file `page.tsx` yang berhubungan.

```typescript
export default function TourDetailPage({
  params,
}: Readonly<{
  params: Promise<{ id: string }>
}>) {
  try {
    const res = await customFetch<TourDetailResponse>(`/tour/${params.id}`)
    
    if (res.statusCode === 404 || res.statusCode === 403) {
      return notFound()
    }
    
    return <DetailTourModule initialData={res.data} />
  } catch (err) {
    return notFound()
  }
}
```

Kemudian update komponen `DetailTourModule` untuk menerima data yang sudah di-fetch:

```typescript
// Di DetailTourModule
export default function DetailTourModule({ initialData }: { initialData: Tour }) {
  const [data, setData] = useState<Tour>(initialData)
  // ...
}
```

Dengan cara ini, segi performa akan meningkat karena data sudah tersedia saat halaman pertama kali dimuat, mengurangi kebutuhan JavaScript di sisi klien. Pendekatan server-side juga meningkatkan [SEO](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) karena konten dapat diindeks dengan lebih baik oleh mesin pencari. Loading state awal juga dapat dihindari sehingga skor [Content Layout Shift](https://web.dev/articles/cls) dapat berkurang, serta waktu interaktif halaman yang lebih cepat. Semua ini berkontribusi pada [Core Web Vitals](https://developers.google.com/search/docs/appearance/core-web-vitals) yang lebih baik dan pengalaman pengguna yang optimal.

Sebagai contoh, mungkin kamu dapat melihat halaman profile yang sudah saya buat, disitu saya memanggil konten profile dengan cara _server-sided_ . Jika kamu butuh bantuan, jangan sungkan untuk bertanya ya!
created_at: 2025-05-09T18:35:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+'use client'
+
+export const TourOrderCard = ({
+  pricePerTicket,
+}: {
+  pricePerTicket: number
+}) => {
+  return (
+    <div className="sticky top-4 w-full lg:w-72 bg-white p-4 rounded-xl shadow-md self-start">
+      <p className="text-xl font-semibold text-gray-800 text-raleway">
+        Rp{pricePerTicket}
path: src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
user: login: fahmi-ramadhan
body: Akan lebih baik jika harganya ini ditampilkan dengan separator ribuan agar lebih mudah dibaca. Untuk memperbaikinya, bisa seperti ini: `Rp{pricePerTicket.toLocaleString('id-ID')}`
created_at: 2025-05-12T05:19:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+'use client'
+
+export const TourOrderCard = ({
+  pricePerTicket,
+}: {
+  pricePerTicket: number
+}) => {
+  return (
+    <div className="sticky top-4 w-full lg:w-72 bg-white p-4 rounded-xl shadow-md self-start">
+      <p className="text-xl font-semibold text-gray-800 text-raleway">
+        Rp{pricePerTicket}
+        <span className="text-sm"> / pax</span>
+      </p>
+      <button className="mt-4 w-full bg-gradient-to-r from-[#016CD7] to-[#014285] text-white py-2 rounded-lg">
+        Pesan Sekarang
+      </button>
path: src/modules/DetailTourModule/module-elements/TourOrderCard.tsx
user: login: fahmi-ramadhan
body: Untuk button, sebaiknya gunakan saja dari komponen shadcn dengan modifikasi sedikit untuk styling-nya agar seragam dengan button-button lainnya yang digunakan di website. Selain itu, agar lebih baik accessibilitynya mungkin bisa ditambahkan aria-label, misalnya `aria-label="Book this tour now"`.
created_at: 2025-05-12T05:22:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
path: src/modules/DetailTourModule/index.tsx
user: login: matthewhjt
body: Halo, saya lihat di sini ada import TourInclude, tapi di module-elements tidak ada TourInclude
created_at: 2025-05-12T14:28:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
path: src/modules/DetailTourModule/index.tsx
user: login: matthewhjt
body: Sama seperti TourInclude, saya juga tidak melihat ada TourSchedule dari module elements.
created_at: 2025-05-12T14:29:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+'use client'
+import { TourHeader } from './module-elements/TourHeader'
+import { TourDescription } from './module-elements/TourDescription'
+import { TourInclude } from './module-elements/TourInclude'
+import { TourOrderCard } from './module-elements/TourOrderCard'
+import { useEffect, useState } from 'react'
+import { customFetch } from '@/utils/newCustomFetch'
+import { Loader2 } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import { TourSchedule } from './module-elements/TourSchedule'
+import { TourList } from './module-elements/TourList'
+
+export default function DetailTourModule() {
+  const [data, setData] = useState<Tour | null>(null)
+  const [isNotFound, setIsNotFound] = useState(false)
+  const { id } = useParams<{ id: string }>()
+
+  const fetchData = async () => {
+    try {
+      const res = await customFetch<TourDetailResponse>(`/tour/${id}`, {
+        credentials: 'include',
+      })
+
+      if (res.statusCode === 404 || res.statusCode === 403) {
+        setIsNotFound(true)
+      }
+
+      setData(res.data)
+    } catch (err: any) {
+      setIsNotFound(true)
+    }
+  }
+
+  useEffect(() => {
+    void fetchData()
+  }, [id])
+
+  return data ? (
+    <div className="max-w-6xl mx-auto p-4 pt-24 py-8">
+      <TourHeader
+        title={data.title}
+        location={data.location}
+        coverImage={data.coverImage}
+      />
+      <div className="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
+        <div className="lg:col-span-2 space-y-6">
+          <TourDescription
+            title={data.title}
+            description={data.description}
+            maxCapacity={data.maxCapacity}
+          />
+          {/* <TourInclude includes={[]} /> */}
+          {/* <TourSchedule/> */}
path: src/modules/DetailTourModule/index.tsx
user: login: matthewhjt
body: Di sini sepertinya kamu mau membuat komponen TourInclude dan TourSchedule, tetapi tidak jadi, atau belum dipush ya? Tolong diperbaiki atau dihapus kalau tidak jadi, terima kasih.
created_at: 2025-05-12T14:30:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,28 @@
+'use client'
+
+import { Home, Bus, Ticket, Utensils, Users } from 'lucide-react'
+import { JSX } from 'react'
+
+const iconMap: Record<string, JSX.Element> = {
+  ticket: <Ticket className="text-blue-500" />,
+  utensils: <Utensils className="text-blue-500" />,
+  home: <Home className="text-blue-500" />,
+  bus: <Bus className="text-blue-500" />,
+  users: <Users className="text-blue-500" />,
+}
+
+export const TourInclude = ({ includes }: { includes: Include[] }) => {
+  return (
+    <div>
+      <h3 className="text-base font-semibold mb-2">Sudah Termasuk</h3>
+      <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
+        {includes.map((item) => (
+          <div key={item.id} className="flex items-center gap-2">
+            {iconMap[item.icon] || <span className="w-4" />}
+            <span>{item.text}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
path: src/modules/DetailTourModule/module-elements/TourInclude.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing import for Include type**

The component uses the `Include` type, but there's no import statement for it. You should import this type from your type definition file to ensure type safety.

```diff
'use client'

import { Home, Bus, Ticket, Utensils, Users } from 'lucide-react'
import { JSX } from 'react'
+import { Include } from '../type'
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
'use client'

import { Home, Bus, Ticket, Utensils, Users } from 'lucide-react'
import { JSX } from 'react'
import { Include } from '../type'

const iconMap: Record<string, JSX.Element> = {
  ticket: <Ticket className="text-blue-500" />,
  utensils: <Utensils className="text-blue-500" />,
  home: <Home className="text-blue-500" />,
  bus: <Bus className="text-blue-500" />,
  users: <Users className="text-blue-500" />,
}

export const TourInclude = ({ includes }: { includes: Include[] }) => {
  return (
    <div>
      <h3 className="text-base font-semibold mb-2">Sudah Termasuk</h3>
      <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
        {includes.map((item) => (
          <div key={item.id} className="flex items-center gap-2">
            {iconMap[item.icon] || <span className="w-4" />}
            <span>{item.text}</span>
          </div>
        ))}
      </div>
    </div>
  )
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+import notFound from '@/app/not-found'
path: src/app/(main)/tour/[id]/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Use correct import for notFound**

Import `notFound` from 'next/navigation' instead of from '@/app/not-found' to follow Next.js conventions.

```diff
-import notFound from '@/app/not-found'
+import { notFound } from 'next/navigation'
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+import notFound from '@/app/not-found'
+import DetailTourModule from '@/modules/DetailTourModule'
+import { customFetch } from '@/utils/newCustomFetch'
+
+export default async function TourDetailPage({
+  params,
+}: Readonly<{
+  params: Promise<{ id: string }>
+}>) {
path: src/app/(main)/tour/[id]/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix params type definition**

The params shouldn't be typed as a Promise. In Next.js App Router, params are direct objects.

```diff
export default async function TourDetailPage({
  params,
}: Readonly<{
-  params: Promise<{ id: string }>
+  params: { id: string }
}>) {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export default async function TourDetailPage({
  params,
}: Readonly<{
  params: { id: string }
}>) {
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+import notFound from '@/app/not-found'
+import DetailTourModule from '@/modules/DetailTourModule'
+import { customFetch } from '@/utils/newCustomFetch'
+
+export default async function TourDetailPage({
+  params,
+}: Readonly<{
+  params: Promise<{ id: string }>
+}>) {
+  try {
+    const res = await customFetch<TourDetailResponse>(
+      `/tour/${(await params).id}`
+    )
+
+    if (res.statusCode === 404 || res.statusCode === 403) {
+      return notFound()
+    }
+
+    return <DetailTourModule initialData={res.data} />
+  } catch (err) {
+    return notFound()
+  }
+}
path: src/app/(main)/tour/[id]/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing import for TourDetailResponse type**

The component uses the `TourDetailResponse` type in the customFetch call, but there's no import statement for it.

```diff
import { notFound } from 'next/navigation'
import DetailTourModule from '@/modules/DetailTourModule'
import { customFetch } from '@/utils/newCustomFetch'
+import { TourDetailResponse } from '@/modules/DetailTourModule/type'
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,23 @@
+import notFound from '@/app/not-found'
+import DetailTourModule from '@/modules/DetailTourModule'
+import { customFetch } from '@/utils/newCustomFetch'
+
+export default async function TourDetailPage({
+  params,
+}: Readonly<{
+  params: Promise<{ id: string }>
+}>) {
+  try {
+    const res = await customFetch<TourDetailResponse>(
+      `/tour/${(await params).id}`
+    )
path: src/app/(main)/tour/[id]/page.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Simplify params access**

Since params is not a Promise, you can access the id directly without awaiting it.

```diff
try {
  const res = await customFetch<TourDetailResponse>(
-    `/tour/${(await params).id}`
+    `/tour/${params.id}`
  )
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,100 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Define or import Route interface**

The `Block` interface references `Route` but there's no definition or import for it.

```diff
+interface Route {
+  sourceBlockId: string
+  destinationBlockId: string
+  distance: number
+  duration: number
+  polyline?: string
+  transportMode?: string
+}
+
interface Block {
  // ...
  routeToNext?: Route
  routeFromPrevious?: Route
  // ...
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
interface Route {
  sourceBlockId: string
  destinationBlockId: string
  distance: number
  duration: number
  polyline?: string
  transportMode?: string
}

interface Block {
  // ...
  routeToNext?: Route
  routeFromPrevious?: Route
  // ...
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,100 @@
+interface Block {
+  updatedAt: string
+  createdAt: string
+  id: string
+  sectionId: string
+  position: number
+  blockType: string
+  title: string
+  description: string
+  startTime: string
+  endTime: string
+  location: string
+  price: number
+  photoUrl: string | undefined
+  routeToNext?: Route
+  routeFromPrevious?: Route
+}
path: src/modules/DetailTourModule/type.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Align Block interface with existing definition**

The `Block` interface you've defined differs from the one in `src/modules/ItineraryMakerModule/interface.ts`. Consider aligning them or extending the existing one.

```diff
+import { Block as BaseBlock } from '@/modules/ItineraryMakerModule/interface'

-interface Block {
+interface Block extends Omit<BaseBlock, 'description' | 'startTime' | 'endTime' | 'location' | 'price'> {
  updatedAt: string
  createdAt: string
  id: string
  sectionId: string
  position: number
-  blockType: string
  title: string
-  description: string
-  startTime: string
-  endTime: string
-  location: string
-  price: number
+  description: string  // Make required instead of optional
+  startTime: string    // Make required instead of optional
+  endTime: string      // Make required instead of optional
+  location: string     // Make required instead of optional
+  price: number        // Make required instead of optional
  photoUrl: string | undefined
-  routeToNext?: Route
-  routeFromPrevious?: Route
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { Block as BaseBlock } from '@/modules/ItineraryMakerModule/interface'

interface Block extends Omit<BaseBlock, 'description' | 'startTime' | 'endTime' | 'location' | 'price'> {
  updatedAt: string
  createdAt: string
  id: string
  sectionId: string
  position: number
  title: string
  description: string  // Make required instead of optional
  startTime: string    // Make required instead of optional
  endTime: string      // Make required instead of optional
  location: string     // Make required instead of optional
  price: number        // Make required instead of optional
  photoUrl: string | undefined
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T15:06:27Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-09T10:29:23Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a comprehensive set of new React components and TypeScript types for a detailed tour module, along with corresponding unit tests. The changes include a new tour detail page, a data-fetching module, UI components for displaying tour information, and a suite of tests to verify rendering and interaction behaviors. No existing exports are modified.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                                     |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `src/app/(main)/tour/[id]/page.tsx`                                                        | Adds a new React page component (`TourDetailPage`) that fetches tour data by ID, handles errors with `notFound()`, and renders the `DetailTourModule` for tour detail display.                                                                    |
| `src/modules/DetailTourModule/index.tsx`                                                    | Introduces `DetailTourModule`, a client component that accepts initial tour data and renders the main tour detail layout including header, description, order card, and itinerary list; shows a loading spinner if data is not present.             |
| `src/modules/DetailTourModule/module-elements/TourHeader.tsx`                               | Adds `TourHeader` component to display the tour title, location, and a cover image or fallback gradient background.                                                                                                                                |
| `src/modules/DetailTourModule/module-elements/TourDescription.tsx`                          | Adds `TourDescription` component with expandable/collapsible description and tour capacity indicator with icon.                                                                                                                                     |
| `src/modules/DetailTourModule/module-elements/TourOrderCard.tsx`                            | Adds `TourOrderCard` component to display ticket pricing formatted in Indonesian Rupiah and a "Pesan Sekarang" (Order Now) button.                                                                                                                |
| `src/modules/DetailTourModule/module-elements/TourList.tsx`                                 | Adds `TourList` component that renders a list of itinerary sections by mapping over `TourDay` components; returns null if sections are empty or falsy.                                                                                              |
| `src/modules/DetailTourModule/module-elements/TourDay.tsx`                                  | Adds `TourDay` component rendering a timeline view of a single tour day with conditional timeline markers, icons, and block rendering based on block type (`LOCATION` or `NOTE`), including formatted times and descriptions.                        |
| `src/modules/DetailTourModule/module-elements/TourInclude.tsx`                              | Adds `TourInclude` component rendering a grid of included tour features with icons mapped from string keys to lucide-react icons, displaying icon and text for each included item.                                                                 |
| `src/modules/DetailTourModule/type.ts`                                                      | Introduces TypeScript interfaces for tour details, itinerary, sections, blocks, includes, and API response types used in the module.                                                                                                               |
| `src/modules/DetailItineraryModule/module-elements/ItineraryHeader.tsx`                     | Adds dynamic Open Graph meta tags in the document head when sharing or inviting dialogs are open, based on itinerary data's title, description, and cover image.                                                                                   |
| `__tests__/TourHeader.test.tsx`                                                             | Adds tests for `TourHeader` to verify rendering of title, location, and conditional background image or gradient styles.                                                                                                                           |
| `__tests__/TourDescription.test.tsx`                                                        | Adds tests for `TourDescription` to verify truncated/full description toggle behavior and capacity display with icon.                                                                                                                              |
| `__tests__/TourOrderCard.test.tsx`                                                          | Adds tests for `TourOrderCard` verifying correct price formatting and presence of the order button.                                                                                                                                                 |
| `__tests__/TourList.test.tsx`                                                               | Adds tests for `TourList` to verify rendering of multiple `TourDay` components and handling of empty sections array.                                                                                                                               |
| `__tests__/TourDay.test.tsx`                                                                | Adds tests for `TourDay` verifying timeline marker rendering, conditional icon display, and correct rendering of `LOCATION` and `NOTE` block types.                                                                                                |
| `src/modules/DetailTourModule/page.tsx`                                                    | Removed obsolete placeholder component `DetailTourModule` that rendered an empty fragment.                                                                                                                                                          |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant TourDetailPage
    participant DetailTourModule
    participant API
    participant TourHeader
    participant TourDescription
    participant TourOrderCard
    participant TourList
    participant TourDay

    User->>TourDetailPage: Navigates to /tour/[id]
    TourDetailPage->>API: Fetch /tour/{id}
    API-->>TourDetailPage: Return tour data or error
    alt error (404 or 403)
        TourDetailPage->>User: Render notFound page
    else success
        TourDetailPage->>DetailTourModule: Render with initialData
        DetailTourModule->>TourHeader: Render header
        DetailTourModule->>TourDescription: Render description
        DetailTourModule->>TourOrderCard: Render order card
        DetailTourModule->>TourList: Render itinerary list
        TourList->>TourDay: Render each section
    end
```

## Poem

> 🐇  
> In fields of code, a tour unfolds,  
> With headers, lists, and tales retold.  
> Descriptions grow with just a click,  
> Order cards and prices stick.  
> Tests abound to check each view—  
> A journey built for me and you!  
> 🌄✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Lite**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ce1c2054d70bf453b3d399d4a8d11909e633f6f7 and 2df10afe581675ec0913744f586904a29969c5b1.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `__tests__/TourOrderCard.test.tsx` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* __tests__/TourOrderCard.test.tsx

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Mutari-App/mutari-frontend&utm_content=190):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: daffarafi
created_at: 2025-05-09T18:40:09Z
body: ![image](https://github.com/user-attachments/assets/f803895a-e9a2-4639-b15c-829526a127f0)

Selain itu, jangan lupa perbaiki error yang terjadi ketika melakukan _build_ ya! untuk melakukan build bisa jalankan command `pnpm run build`
author_association: MEMBER