# Twitter Feed Carousel Widget

> Swiper-based carousel of tweets from a user account or hashtag, fetched via Twitter API v2 (bearer token) or legacy v1.1 (consumer key + secret).

**Class file:** `includes/Elements/Twitter_Feed_Carousel.php` (1,558 lines)
**Slug:** `twitter-feed-carousel` (widget id `eael-twitter-feed-carousel`)
**Public docs:** <https://essential-addons.com/elementor/docs/twitter-feed-carousel/>
**Pro-shared:** Pro-only. **External integration: Twitter API v2 + v1.1**.

## Overview

Renders a Swiper carousel of recent tweets from a configured Twitter account (or hashtag, on v1.1). Twitter API access required — bearer token (v2 preferred) OR consumer key + secret (v1.1 fallback). Renders use a shared `twitter_feed_render_items()` helper composed from **Lite's** `Traits/Twitter_Feed.php` — Pro doesn't own the fetch logic.

## Pro vs Lite

This is a Pro-only **widget**, but the data-fetching logic lives in **Lite's** `Twitter_Feed` trait. The widget composes that trait via `use \Essential_Addons_Elementor\Traits\Twitter_Feed;`. Editing the fetch logic requires editing Lite, not Pro.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Twitter_Feed_Carousel.php` | Widget class — controls + render. Uses Lite's `Twitter_Feed` trait. |
| `../essential-addons-for-elementor-lite/includes/Traits/Twitter_Feed.php` | Trait — `twitter_feed_render_items()`, OAuth v2 token fetch, v1.1 timeline fetch, v2 user-lookup |
| `src/js/view/twitter-feed-carousel.js` → `assets/front-end/js/view/twitter-feed-carousel.min.js` | Swiper init |
| `../essential-addons-for-elementor-lite/assets/front-end/css/view/twitter-feed.min.css` | **Lite-owned** CSS — referenced via `EAEL_PLUGIN_PATH` in Pro's `config.php` |
| `config.php` entry `'twitter-feed-carousel'` | Mixed Lite CSS + Pro JS |

## Architecture

- **Composes Lite's `Twitter_Feed` trait** — `Twitter_Feed_Carousel.php:21`. This is the canonical "Pro depends on a Lite helper" pattern. When Lite refactors `Twitter_Feed`, this widget breaks. See [`_patterns.md § Helper imports`](_patterns.md#helper-imports).
- **API version split:**
  - **v2 preferred** — bearer token via `eael_twitter_feed_bearer_token` control. Endpoints: `https://api.twitter.com/2/users/by/username/{name}`, `/2/users/{id}/tweets`, etc.
  - **v1.1 fallback** — consumer key + secret → bearer token via `oauth2/token`. Endpoint: `https://api.twitter.com/1.1/statuses/user_timeline.json`. Lite trait line 51 (`oauth2/token` POST), line 72 (user_timeline GET).
- **Cache key** — Lite trait `:23`: `{account_name}_{expiration}_{md5(hashtag + consumer_key + consumer_secret + bearer_token)}_tf_cache`. TTL configurable.
- **Tweet HTML escaped via `wp_kses( $render_html, Helper::eael_allowed_tags() )`** in widget's `render()` (line 1539) — uses Pro's `Helper::eael_allowed_tags()` whitelist.
- **Swiper carousel** — uses Swiper v8. Widget renders `class="swiper swiper-8"`. See [`.claude/rules/widget-development.md`](../../.claude/rules/widget-development.md) on the Swiper handle pattern.
- **No timeouts** on Lite-side `wp_remote_get/post` for Twitter — uses WP default 5s.

## Render Output

```html
<div class="eael-twitter-feed eael-twitter-feed-carousel swiper swiper-8 eael-twitter-feed-{element-id}"
     data-items="3"
     data-items-tablet="2"
     data-items-mobile="1"
     data-margin="20"
     data-effect="slide"
     data-speed="500"
     data-autoplay="3000"
     data-pause-on-hover="true"
     data-dots="1">
  <div class="swiper-wrapper">
    <!-- One swiper-slide per tweet -->
    <div class="swiper-slide eael-twitter-feed-item">
      <div class="eael-twitter-feed-item__header">
        <img src="{user_avatar}" />
        <span class="username">@{handle}</span>
      </div>
      <div class="eael-twitter-feed-item__text">{tweet_text}</div>
      <div class="eael-twitter-feed-item__footer">
        <time>{posted_at}</time>
      </div>
    </div>
  </div>
  [?] <div class="swiper-pagination swiper-pagination-{element-id}"></div>
  [?] <div class="swiper-button-next">...</div>
  [?] <div class="swiper-button-prev">...</div>
</div>
```

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `eael_twitter_feed_ac_name` | Content → Twitter | TEXT | Account name (handle, no `@`) |
| `eael_twitter_feed_hashtag_name` | Content → Twitter | TEXT | Hashtag (v1.1 only) |
| `eael_twitter_feed_consumer_key` | Content → Twitter | TEXT | OAuth consumer key (v1.1) |
| `eael_twitter_feed_consumer_secret` | Content → Twitter | TEXT | OAuth consumer secret (v1.1) |
| `eael_twitter_feed_bearer_token` | Content → Twitter | TEXT | Bearer token (v2) |
| `items` / `items_tablet` / `items_mobile` | Content → Carousel | SLIDER | Per-breakpoint visible items |
| `slider_speed`, `autoplay`, `autoplay_speed`, `pause_on_hover` | Content → Carousel | various | Swiper controls |
| `carousel_effect` | Content → Carousel | SELECT | Slide / fade / cube / coverflow |
| `arrows` / `dots` | Content → Carousel | SWITCHER | Navigation visibility |

## Conditional Dependencies

```text
autoplay = 'yes'
  └── shows autoplay_speed slider
  └── shows pause_on_hover toggle

arrows = 'yes'
  └── shows arrow_left + arrow_right icon pickers
```

API version is auto-detected — if `bearer_token` is present, v2 is used (Lite trait `:34`). Otherwise v1.1.

## JavaScript Lifecycle

Standard Swiper init pattern — see [`.claude/rules/widget-development.md`](../../.claude/rules/widget-development.md) "Swiper init pattern":

```js
var TwitterFeedCarousel = function( $scope, $ ) {
    var $carousel = $scope.find( '.eael-twitter-feed-carousel' );
    var config = { /* read from data-* attrs */ };
    swiperLoader( $carousel[0], config );
};
```

Uses Elementor's `elementorFrontend.utils.swiper` async loader, not a bundled Swiper.

## Hooks & Filters

### Elementor hooks consumed

Standard widget render.

### Pro / EA hooks emitted

None directly. Lite's `Twitter_Feed` trait may emit hooks — verify in Lite.

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| Empty carousel | Twitter API returned no tweets / auth failed | Check transient cache; inspect last API response in Lite trait | Verify bearer token validity; confirm account name is correct |
| "App suspended" or 403 | Twitter API access revoked / rate-limited | Twitter developer portal | Wait out rate limit; rotate token |
| Slow page render | API call hit per page-load when cache TTL = 0 | Reduce calls by setting longer TTL | Set `expiration` ≥ 1 hour |
| Tweets show in wrong order | API v2 vs v1.1 ordering difference | Check which API is being hit | Pick one and stick with it; document for user |
| Photos / media missing | v2 expansion fields not requested | Inspect Lite trait's v2 fetch | Add `expansions=attachments.media_keys&media.fields=url,preview_image_url` to v2 endpoint (Lite-side fix) |

## Known Limitations

- **API v1.1 is deprecated by Twitter / X.** v1.1 may stop responding at any time
- **Bearer token in widget settings** — same plaintext-in-post-meta risk as Mailchimp / Instagram
- **No timeouts** on Twitter calls — default 5s
- **No rate-limit honour** — burst pages can hit API limit
- **Trait composition makes refactor coupling** — `Pro depends on Lite/Twitter_Feed`. See `/pro-lite-sync`
- **No `prefers-reduced-motion`** for autoplay carousel
- **Hashtag mode requires v1.1** — v2 endpoint for search isn't wired

## Cross-References

- Architecture: [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md) — Twitter surface
- Architecture: [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md) — Lite trait composition pattern
- Shared patterns: [`_patterns.md`](_patterns.md)
- Skill: [`/pro-lite-sync`](../../.claude/skills/pro-lite-sync/SKILL.md) — relevant for Lite trait changes
