# Call Card Display Reference

This document describes when and how call cards are displayed on the platform, covering guest vs authenticated contexts, component variants, layout options, scoring logic, and platform configuration.

---

## Overview

Call cards appear in two page contexts:

| Page | Audience | Components used |
|------|----------|-----------------|
| Welcome page (`/`) | Guests (unauthenticated) | `WelcomePage\CallCardHalf`, `WelcomePage\CallCardCarousel` (in CTA post) |
| Main dashboard | Authenticated users | `MainPage\CallCardCarousel`, `MainPage\CallCardHalf`, `MainPage\CallCardFull` |

---

## Component Map

```
welcome.blade.php
├── @livewire('welcome-page.call-card-half', ['random' => true, 'rows' => 2])
│   └── WelcomePage\CallCardHalf
│       └── view: livewire/main-page/call-card-half.blade.php
│           └── x-call-card (components/call-card.blade.php) × N
│
└── @livewire('welcome.cta-post')
    └── cta-post.blade.php
        └── @livewire('welcome-page.call-card-carousel', ['random' => false])
            └── WelcomePage\CallCardCarousel
                └── view: livewire/main-page/call-card-carousel.blade.php

main-page.blade.php (authenticated)
├── @livewire('main-page.call-card-carousel', ['related' => true, 'random' => false])
│   └── MainPage\CallCardCarousel
│       └── view: livewire/main-page/call-card-carousel.blade.php
│
├── @livewire('main-page.call-card-half', ['related' => false, 'random' => true, 'rows' => 2])
│   └── MainPage\CallCardHalf
│       └── view: livewire/main-page/call-card-half.blade.php
│           └── x-call-card (components/call-card.blade.php) × N
│
└── [individual card sections use livewire/main-page/call-card-full.blade.php]
```

---

## Guest vs Authenticated Distinction

### Guest (Welcome page)

Components: `WelcomePage\CallCardCarousel`, `WelcomePage\CallCardHalf`

- `is_public = true` is **always hardcoded** — cannot be overridden by config
- No profile context → no locality filtering applied
- `CallCarouselScorer` is instantiated with `null` for all location IDs
- Uses config block `calls.welcome_carousel`
- Location proximity boost factors have no effect (no profile location to compare against)
- Reaction buttons: displayed as read-only (no click to react). When `like_count > 0`, a solid filled icon is shown in white. When `like_count = 0`, the reaction button is hidden entirely.

### Authenticated (Main dashboard)

Components: `MainPage\CallCardCarousel`, `MainPage\CallCardHalf`

- `is_public` enforcement controlled by `calls.carousel.exclude_non_public` (platform config)
- Profile resolved via `getActiveProfile()` → location extracted for locality filtering
- `CallCarouselScorer` receives profile city/division/country IDs → location proximity boosts apply
- Uses config block `calls.carousel`
- Own calls excluded when `calls.carousel.exclude_own_calls = true`
- Reaction buttons: fully interactive (like/unlike)

---

## Shared Query Filters (all variants)

The following filters are always hardcoded regardless of config or auth state:

```php
->whereNull('deleted_at')
->where('is_paused', false)
->where('is_suppressed', false)
->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now()))
```

---

## Locality Filtering (authenticated only)

When a profile has a location, the auth carousel and half-card components build location ID arrays and add a WHERE clause to restrict calls to nearby locations.

**Location resolution hierarchy:**

1. **City level** — `location.city` is set → expands to all sibling cities in the same division (when `related = true`)
2. **Division level** — `location.division` set but no city → expands to all sibling divisions in same parent (when `related = true`)
3. **Country level** — only country set → all countries (when `related = true`) or just the profile's country

**Locality config keys** (under `calls.carousel`):

| Key | Default | Effect |
|-----|---------|--------|
| `include_unknown_location` | `true` | Also include calls with unknown/unspecified location (`country_id = 10`) |
| `include_same_division` | `true` | Include calls in same division as profile |
| `include_same_country` | `true` | Include calls in same country as profile |

---

## Scoring — CallCarouselScorer

All variants use `App\Http\Livewire\Calls\CallCarouselScorer`. The scorer multiplies a base score of `1.0` by a series of boost factors read from config.

### Location proximity boosts (authenticated only — null IDs → no effect)

| Config key | Default | Applied when |
|------------|---------|--------------|
| `boost_same_district` | 3.0 | Call location city matches profile city |
| `boost_location_city` | 2.0 | Call has city-level location |
| `boost_location_division` | 1.5 | Call has division-level location |
| `boost_location_country` | 1.1 | Call has country-level location |
| `boost_location_unknown` | 0.8 (default) / 2.5 (timebank_cc) | Call has unknown location |

### Engagement boosts

| Config key | Default | Applied when |
|------------|---------|--------------|
| `boost_like_count` | 0.05 | Multiplied by call's like count |
| `boost_star_count` | 0.10 | Multiplied by callable's star count |

### Recency / urgency boosts

| Config key | Default | Applied when |
|------------|---------|--------------|
| `boost_recent_from` | 1.3 | Call was created within `recent_days` (14) days |
| `boost_soon_till` | 1.2 | Call expires within `soon_days` (7) days |

### Callable type boosts

| Config key | Default | Applied when |
|------------|---------|--------------|
| `boost_callable_user` | 1.0 | Call posted by a User |
| `boost_callable_organization` | 1.2 | Call posted by an Organization |
| `boost_callable_bank` | 1.0 | Call posted by a Bank |

### Random jitter

When the component is mounted with `random = true`, the final score is multiplied by `random_int(85, 115) / 100`, introducing ±15% variation to shuffle order on each page load.

---

## Pool and Selection Logic

All components fetch a **candidate pool** larger than the number of cards to display, score all candidates in PHP, then take the top N by score.

```
pool_size = display_limit × pool_multiplier
```

| Component | display_limit | pool_multiplier config key |
|-----------|--------------|---------------------------|
| `CallCardCarousel` (auth) | `max_cards` | `calls.carousel.pool_multiplier` |
| `CallCardCarousel` (guest) | `max_cards` | `calls.welcome_carousel.pool_multiplier` |
| `CallCardHalf` (auth) | `rows × 2` | `calls.carousel.pool_multiplier` |
| `CallCardHalf` (guest) | `rows × 2` | `calls.welcome_carousel.pool_multiplier` |

---

## Platform Configuration

### `calls.carousel` — authenticated main-page carousel and half cards

```php
'carousel' => [
    'max_cards'                   => 12,
    'pool_multiplier'             => 5,
    'exclude_non_public'          => true,   // false in timebank_cc (shows private calls to auth users)
    'exclude_own_calls'           => true,
    'include_unknown_location'    => true,
    'include_same_division'       => true,
    'include_same_country'        => true,

    // Scoring boosts
    'boost_same_district'         => 3.0,
    'boost_location_city'         => 2.0,
    'boost_location_division'     => 1.5,
    'boost_location_country'      => 1.1,
    'boost_location_unknown'      => 0.8,   // 2.5 in timebank_cc
    'boost_like_count'            => 0.05,
    'boost_star_count'            => 0.10,
    'boost_recent_from'           => 1.3,
    'recent_days'                 => 14,
    'boost_soon_till'             => 1.2,
    'soon_days'                   => 7,
    'boost_callable_user'         => 1.0,
    'boost_callable_organization' => 1.2,
    'boost_callable_bank'         => 1.0,

    'show_score'                  => false,
    'show_score_for_admins'       => true,
],
```

### `calls.welcome_carousel` — guest welcome page carousel and half cards

```php
'welcome_carousel' => [
    'max_cards'                   => 12,
    'pool_multiplier'             => 5,

    // No location boosts — no profile context for guests
    'boost_like_count'            => 0.05,
    'boost_star_count'            => 0.10,
    'boost_recent_from'           => 1.3,
    'recent_days'                 => 14,
    'boost_soon_till'             => 1.2,
    'soon_days'                   => 7,
    'boost_callable_user'         => 1.0,
    'boost_callable_organization' => 1.2,
    'boost_callable_bank'         => 1.0,

    'show_score'                  => false,
    'show_score_for_admins'       => true,
],
```

**Key difference:** `welcome_carousel` has no `exclude_non_public` key because `is_public = true` is hardcoded in the component. It also has no location keys because locality filtering is never applied on the guest page.

---

## Visual Layouts

### Carousel (`call-card-carousel.blade.php`)

Horizontal scrollable strip. Cards are small (170px tall, ~1/3 viewport wide, min 200px, max 320px). Intended for quick browsing. Alpine.js handles smooth scroll with left/right navigation buttons that appear on hover and hide at scroll boundaries.

Each card shows:
- Deepest (leaf) tag category badge
- Call title (truncated)
- Location and expiry badges
- Callable avatar and name
- Reaction button (top-right, `w-5 h-5`)
- Score (bottom-right, admin-only)

### Half cards (`call-card-half.blade.php` + `x-call-card`)

Responsive grid: 1 column on mobile, 2 columns on `md+`. Each row contains 2 cards; `rows` prop controls how many rows are shown.

The `x-call-card` Blade component (`components/call-card.blade.php`) is used. Props:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `result` | array | `[]` | Call data array |
| `index` | int | `0` | Used to key nested Livewire components |
| `href` | string\|null | `null` | Override link URL |
| `wireClick` | string\|null | `null` | Use `wire:click` instead of href |
| `showScore` | bool | `false` | Show score badge (bottom-right) |
| `showCallable` | bool | `true` | Show callable avatar + name section |
| `showReactions` | bool | `true` | Show reaction (like) button |
| `heightClass` | string | `h-[430px] md:h-[550px] lg:h-[430px]` | Tailwind height classes |
| `truncateExcerpt` | bool | `false` | Clamp excerpt to 2 lines |

Each card shows:
- Tag color background with `bg-black/50` overlay
- Deepest (leaf) tag category badge
- Call title (2-line clamp)
- Location and expiry badges
- Excerpt (2-line clamp when `truncateExcerpt = true`)
- Callable avatar, name, location (when `showCallable = true`)
- Reaction button top-right (when `showReactions = true`)

### Full card (`call-card-full.blade.php`)

Large hero-style card, full width. Used on the main dashboard for featured individual calls. Taller than half cards with larger typography (`text-3xl`/`text-4xl` title). Like button is absolutely positioned at `top-14 right-4` with `w-10 h-10` size.

---

## Call Data Array Structure

All components produce a uniform array for each call:

```php
[
    'id'                => int,
    'model'             => 'App\Models\Call',
    'title'             => string,   // tag name in active locale
    'excerpt'           => string,   // call translation content
    'photo'             => string,   // callable profile photo URL
    'location'          => string,   // "City, COUNTRY" or null
    'tag_color'         => string,   // Tailwind color name (e.g. 'green', 'blue')
    'tag_categories'    => [         // ancestor chain from root to leaf
        ['name' => string, 'color' => string],
        ...
    ],
    'callable_name'     => string,
    'callable_location' => string,   // built from callable's profile location
    'till'              => datetime|null,
    'expiry_badge_text' => string|null,  // e.g. "Expires in 5 days"
    'like_count'        => int,
    'score'             => float,
]
```

Only the **deepest** (last) entry in `tag_categories` is displayed as a badge on the card.

---

## Reaction Button Behaviour on Call Cards

| Context | State | Behaviour |
|---------|-------|-----------|
| Guest, `like_count = 0` | Hidden | Entire reaction button hidden |
| Guest, `like_count > 0` | Read-only | Solid filled icon + count, white (`inverseColors`) |
| Authenticated, not reacted | Interactive | Outline icon, clickable to like |
| Authenticated, reacted | Interactive | Solid filled icon, clickable to unlike |

On the call show page (`calls/show-guest.blade.php`) for guests, the reaction button links to the login page with the current URL as redirect, so clicking the icon takes the guest to login.

---

## File Reference

| File | Role |
|------|------|
| `app/Http/Livewire/MainPage/CallCardCarousel.php` | Auth carousel — fetches, scores, filters by location |
| `app/Http/Livewire/MainPage/CallCardHalf.php` | Auth half-grid — same logic as carousel, `rows` prop |
| `app/Http/Livewire/WelcomePage/CallCardCarousel.php` | Guest carousel — public only, no location filter |
| `app/Http/Livewire/WelcomePage/CallCardHalf.php` | Guest half-grid — public only, no location filter |
| `app/Http/Livewire/Calls/CallCarouselScorer.php` | Scoring engine used by all four components |
| `app/Http/Livewire/Calls/ProfileCalls.php` | Helpers: `buildCallableLocation()`, `buildExpiryBadgeText()` |
| `resources/views/livewire/main-page/call-card-carousel.blade.php` | Carousel UI (shared by auth + guest carousel components) |
| `resources/views/livewire/main-page/call-card-half.blade.php` | Half-grid UI (shared by auth + guest half components) |
| `resources/views/livewire/main-page/call-card-full.blade.php` | Full-width card (main dashboard only) |
| `resources/views/components/call-card.blade.php` | Reusable card component used by half-grid |
| `resources/views/welcome.blade.php` | Guest welcome page layout |
| `resources/views/livewire/welcome/cta-post.blade.php` | CTA section — embeds guest carousel |
| `config/timebank-default.php` | Default `calls.carousel` and `calls.welcome_carousel` config |
| `config/timebank_cc.php` | Platform overrides (`exclude_non_public`, `boost_location_unknown`) |
