Initial commit
This commit is contained in:
335
references/CALL_CARD_DISPLAY_REFERENCE.md
Normal file
335
references/CALL_CARD_DISPLAY_REFERENCE.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# 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`) |
|
||||
Reference in New Issue
Block a user