336 lines
14 KiB
Markdown
336 lines
14 KiB
Markdown
# 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`) |
|