# Jeddah Night API Documentation

**Base URL:** `http://localhost:8000/api/v1`

**Authentication:** Laravel Sanctum — pass `Authorization: Bearer {token}` header for protected endpoints.

**Locale:** Content endpoints use `/{locale}/` prefix where `{locale}` is `ar` or `en`. Translations are returned based on this prefix.

---

## Table of Contents

1. [Authentication](#1-authentication)
2. [Home](#2-home)
3. [Categories](#3-categories)
4. [Places](#4-places)
5. [Events](#5-events)
6. [Stories](#6-stories)
7. [Blog](#7-blog)
8. [Search](#8-search)
9. [Favorites](#9-favorites-auth-required)
10. [Reviews](#10-reviews-auth-required)
11. [Pages](#11-pages)
12. [Settings & Contact](#12-settings--contact)
13. [Error Responses](#13-error-responses)

---

## 1. Authentication

No locale prefix needed for auth endpoints.

### POST `/auth/register`

| Param | Type | Required | Notes |
|-------|------|----------|-------|
| name | string | yes | max 255 |
| email | string | yes | unique, valid email |
| password | string | yes | min 8 chars |
| password_confirmation | string | yes | must match password |

**Response 201:**
```json
{
  "status": true,
  "message": "Registration successful.",
  "data": {
    "user": { "id", "name", "email", "avatar", "phone", "locale", "is_active", "created_at" },
    "token": "string"
  }
}
```

### POST `/auth/login`

| Param | Type | Required |
|-------|------|----------|
| email | string | yes |
| password | string | yes |

**Response 200:** Same structure as register. Returns 401 for invalid credentials, 403 if deactivated.

### POST `/auth/social`

| Param | Type | Required | Notes |
|-------|------|----------|-------|
| provider | string | yes | `google` or `apple` |
| provider_id | string | yes | |
| name | string | yes | |
| email | string | no | |
| avatar | string | no | URL |

**Response 200:** Same structure as register. Creates or links account automatically.

### POST `/auth/phone/send-otp`

| Param | Type | Required |
|-------|------|----------|
| phone | string | yes |

**Response 200:** `{ "status": true, "message": "OTP sent successfully." }`

OTP is 4 digits, valid for 5 minutes.

### POST `/auth/phone/verify-otp`

| Param | Type | Required |
|-------|------|----------|
| phone | string | yes |
| otp | string | yes | 4 digits |

**Response 200:** Same structure as register. Creates user if new.

### POST `/auth/logout` `AUTH`

Deletes current token. **Response 200:** `{ "status": true, "message": "Logged out successfully." }`

### GET `/auth/user` `AUTH`

Returns authenticated user data.

### PUT `/auth/user` `AUTH`

| Param | Type | Required | Notes |
|-------|------|----------|-------|
| name | string | no | max 255 |
| phone | string | no | max 20, unique |
| locale | string | no | `ar` or `en` |
| avatar | file | no | image, max 2MB |

Content-Type: `multipart/form-data` if uploading avatar.

### POST `/auth/fcm-token` `AUTH`

| Param | Type | Required |
|-------|------|----------|
| fcm_token | string | yes |

---

## 2. Home

### GET `/{locale}/home`

Returns all homepage data in a single call.

**Response 200:**
```json
{
  "status": true,
  "data": {
    "sliders": [{
      "id", "title", "subtitle", "image_url",
      "link_type": "place|event|category|url|none",
      "link_id", "link_slug", "link_url", "sort_order"
    }],
    "stories": [{
      "id", "title", "media_type": "image|video",
      "media_url", "thumbnail_url",
      "link_type": "place|event|url|none",
      "link_id", "link_slug", "link_url",
      "starts_at", "expires_at", "sort_order", "views_count"
    }],
    "categories": [{
      "id", "name", "slug", "urls": {"ar","en"},
      "icon", "image", "places_count", "children": [...]
    }],
    "featured_places": [{
      "id", "name", "slug", "urls": {"ar","en"},
      "address", "rating", "reviews_count", "price_level",
      "category": {"id","name"},
      "primary_image", "is_featured", "latitude", "longitude",
      "is_favorited"
    }],
    "upcoming_events": [{
      "id", "title", "slug", "urls": {"ar","en"},
      "image", "start_date", "end_date",
      "location_name", "is_free", "price", "is_featured"
    }]
  }
}
```

---

## 3. Categories

### GET `/{locale}/categories`

Returns parent categories with children.

**Query params:** `per_page` (default 15)

### GET `/{locale}/categories/{slug}`

Returns category with paginated places.

**Query params:** `per_page`, `sort` (`rating|name|price|newest`), `page`

---

## 4. Places

### GET `/{locale}/places`

| Query Param | Type | Default | Notes |
|-------------|------|---------|-------|
| per_page | int | 15 | |
| category_id | int | - | filter |
| search | string | - | searches name in both locales |
| rating_min | float | - | minimum rating |
| price_level | int | - | 1-4 |
| is_featured | bool | - | |
| sort | string | newest | `newest|rating|name|distance` |
| lat | float | - | required for distance sort |
| lng | float | - | required for distance sort |

**Response:** Paginated list of `PlaceListResource`.

### GET `/{locale}/places/{slug}`

Full place details including images, reviews, opening hours, amenities, social links.

Increments `views_count`. Returns 404 if not found.

**Key fields in response:**
```
id, name, slug, urls, description, address,
phone, international_phone, email, website, social_links,
latitude, longitude, google_place_id, google_url,
rating, reviews_count, price_level, business_status,
opening_hours, business_types, editorial_summary,
delivery, dine_in, takeout, reservable,
serves_breakfast, serves_lunch, serves_dinner, serves_coffee,
wifi, wheelchair_accessible, good_for_children, good_for_groups,
parking_options, payment_options,
images: [{ id, image_url, is_primary, sort_order, source }],
reviews: [{ id, rating, comment, author_name, author_photo, source, created_at }],
is_favorited
```

### GET `/{locale}/places/nearby`

| Query Param | Type | Required | Default |
|-------------|------|----------|---------|
| lat | float | yes | |
| lng | float | yes | |
| radius | float | no | 10 (km, max 100) |
| category_id | int | no | |
| limit | int | no | 20 (max 50) |

Returns places sorted by distance (nearest first). No pagination.

---

## 5. Events

### GET `/{locale}/events`

| Query Param | Type | Notes |
|-------------|------|-------|
| per_page | int | default 15 |
| category_id | int | filter |
| is_free | bool | filter |
| date_from | date | Y-m-d format |
| date_to | date | Y-m-d format |
| search | string | searches title |

Only returns active, upcoming events. Sorted by `start_date` ascending.

### GET `/{locale}/events/{slug}`

Full event details including linked place and category. Increments `views_count`.

**Key fields:** `id, title, slug, urls, description, image, start_date, end_date, location_name, latitude, longitude, ticket_url, price, is_free, is_featured, place, category, is_favorited, views_count`

---

## 6. Stories

### GET `/{locale}/stories`

Returns active stories (where `starts_at <= now <= expires_at`), sorted by `sort_order`.

**Response fields:** `id, title, media_type, media_url, thumbnail_url, link_type, link_id, link_slug, link_url, starts_at, expires_at, sort_order, views_count`

---

## 7. Blog

### GET `/{locale}/blog`

| Query Param | Type | Notes |
|-------------|------|-------|
| per_page | int | default 15 |
| category | int | category_id filter |
| featured | bool | only featured posts |

Returns published posts sorted by `published_at` descending.

**Response fields:** `id, title, slug, excerpt, content, image, category, is_featured, views_count, published_at, urls`

### GET `/{locale}/blog/{slug}`

Full blog post. Increments `views_count`. Returns 404 if not found.

---

## 8. Search

### GET `/{locale}/search`

| Query Param | Type | Required |
|-------------|------|----------|
| q | string | yes (min 2 chars) |

**Response 200:**
```json
{
  "status": true,
  "data": {
    "places": [ /* PlaceListResource, max 20 */ ],
    "events": [ /* EventListResource, max 20 */ ]
  }
}
```

Searches place names and event titles in both Arabic and English.

---

## 9. Favorites `AUTH REQUIRED`

### GET `/{locale}/favorites`

Returns user's favorited places and events grouped.

```json
{
  "status": true,
  "data": {
    "places": [ /* PlaceListResource with is_favorited: true */ ],
    "events": [ /* EventListResource */ ]
  }
}
```

### POST `/{locale}/favorites/toggle`

| Param | Type | Required | Values |
|-------|------|----------|--------|
| favorable_type | string | yes | `Place` or `Event` |
| favorable_id | int | yes | must exist |

Toggles: adds if not favorited, removes if already favorited.

**Response:** `{ "status": true, "message": "Added to favorites.", "data": { "is_favorited": true } }`

---

## 10. Reviews `AUTH REQUIRED`

### POST `/{locale}/places/{placeId}/review`

| Param | Type | Required | Notes |
|-------|------|----------|-------|
| rating | int | yes | 1-5 |
| comment | string | no | max 1000 |

One review per user per place. Reviews require admin approval before appearing publicly.

Returns 409 if already reviewed. Returns 404 if place not found.

---

## 11. Pages

### GET `/{locale}/pages/{slug}`

Returns static page content. Only active pages.

**Response fields:** `id, title, slug, urls, content`

---

## 12. Settings & Contact

### GET `/settings`

No locale prefix. Returns public app settings (site name, social links, app store URLs).

### POST `/contact`

No locale prefix. No auth required.

| Param | Type | Required |
|-------|------|----------|
| name | string | yes |
| email | string | yes |
| phone | string | no |
| subject | string | yes |
| message | string | yes |

---

## 13. Error Responses

All errors follow this structure:

```json
{ "status": false, "message": "Error description." }
```

| Code | Meaning |
|------|---------|
| 401 | Unauthenticated or invalid credentials |
| 403 | Account deactivated |
| 404 | Resource not found |
| 409 | Conflict (e.g., duplicate review) |
| 422 | Validation failed (includes `errors` object) |
| 500 | Server error |

**Validation error (422):**
```json
{
  "status": false,
  "message": "Validation failed.",
  "errors": { "field": ["Error message"] }
}
```

---

## Pagination

Paginated endpoints include:
```json
{
  "data": [...],
  "meta": {
    "current_page": 1,
    "last_page": 5,
    "per_page": 15,
    "total": 72
  }
}
```

Use `?per_page=X&page=Y` query parameters.

---

## Notes

- **Images:** All image URLs are absolute (e.g., `http://localhost:8000/storage/places/1/photo.jpg`)
- **Locale fields:** `urls` object on places, events, categories, blog posts contains both Arabic and English API URLs for the same resource
- **Backward compatibility:** Old non-prefixed routes (e.g., `/api/v1/places`) redirect to locale-prefixed versions with 301
- **Favorites:** `is_favorited` field appears on places/events only when authenticated; null otherwise
