Skip to content

Commit 7d8af1a

Browse files
authored
Merge pull request #83 from n49/slider_v1.1.12
added social embed support, fixed image enlarge issue
2 parents e08346e + a9ca39b commit 7d8af1a

11 files changed

Lines changed: 487 additions & 86 deletions

CLAUDE.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# CLAUDE.md
2+
3+
Be extremely concise in all interactions.
4+
5+
## Overview
6+
7+
WordPress plugin for embedding OPIO review feeds and sliders on client websites. Displays reviews from op.io aggregation service.
8+
9+
**Plugin Name:** Widget for OPIO Reviews
10+
**Version:** 1.1.11
11+
12+
## Architecture
13+
14+
### Two Rendering Modes
15+
16+
1. **Feed Shortcode** (`[opio_feed id='X']`)
17+
- Fetches pre-rendered HTML from `feed.op.io`
18+
- No local review rendering - just displays server HTML
19+
- File: `includes/class-feed-shortcode.php`
20+
21+
2. **Slider Widgets** (review carousels)
22+
- Fetches JSON from `op.io/api/entities/reviews-slider`
23+
- Renders reviews locally in PHP templates
24+
- 6 template files (3 public + 3 admin previews):
25+
- `reviews-slider-horizontal-template.php`
26+
- `reviews-slider-horizontal-carousel-template.php`
27+
- `reviews-slider-vertical-template.php`
28+
- `admin-reviews-slider-*` (admin preview versions)
29+
30+
### Key Files
31+
32+
- `opio.php` - Plugin entry point
33+
- `includes/class-plugin.php` - Plugin initialization
34+
- `includes/class-feed-shortcode.php` - Feed shortcode handler
35+
- `includes/class-review-slider.php` - Slider widget handler
36+
- `includes/class-opio-handler.php` - API communication
37+
- `assets/js/opio-main.js` - Feed JS functions
38+
- `assets/js/opio-slider-main.js` - Slider JS functions (lightbox, slick carousel)
39+
40+
### External Dependencies
41+
42+
- Slick Carousel (bundled)
43+
- Moment.js (bundled)
44+
- jQuery (WordPress)
45+
46+
## API Endpoints
47+
48+
- Feed HTML: `https://feed.op.io/reviewFeed/{bizId}` or `/allReviewFeed/{bizId}`
49+
- Slider JSON: `https://op.io/api/entities/reviews-slider`
50+
- Images: `https://images.files.ca/{width}x{height}/{imageId}.jpg?nocrop=1`
51+
- Videos: `https://videocdn.n49.ca/mp4sdpad480p/{videoId}.mp4`
52+
53+
## Media Handling
54+
55+
Current support:
56+
- `rev.images[]` - array of `{imageId}` objects
57+
- `rev.videos[]` - array of `{videoId}` objects
58+
59+
NOT yet supported:
60+
- `rev.embeds[]` - social media embeds (YouTube, etc.)
61+
62+
### JS Functions
63+
64+
In `opio-main.js` (feeds):
65+
- `displayLargeImage(imageId, revId)` - shows large image (has bugs: fixed height, cover mode)
66+
- `addReview()` - builds review HTML dynamically
67+
68+
In `opio-slider-main.js` (sliders):
69+
- `displayLargeImage(imageId, revId)` - different implementation for lightbox
70+
- `openPhotoLightbox(reviewData)` - opens review detail modal
71+
- `hideLargeImage(revId)` - closes large image view
72+
73+
## Shortcodes
74+
75+
```php
76+
[opio_feed id='123'] // Review feed
77+
[opio_slider id='456'] // Review slider
78+
```
79+
80+
## Testing
81+
82+
No automated tests. Manual testing via WordPress admin preview.
83+
84+
## Deployment
85+
86+
Plugin distributed via WordPress plugin directory or direct ZIP upload.

assets/css/public-feed.css

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -974,8 +974,10 @@ height: unset !important;
974974
}
975975

976976
.widget-body .review-img{
977-
width: 48px;
977+
width: 48px;
978978
height: 48px;
979+
object-fit: cover;
980+
border-radius: 4px;
979981
}
980982

981983
.widget-body .video-icon {
@@ -1246,7 +1248,7 @@ height: unset !important;
12461248
}
12471249

12481250
.lb-photo-div {
1249-
display: flex;
1251+
display: flex;
12501252
}
12511253

12521254
.lb-review-property {
@@ -1452,6 +1454,7 @@ height: unset !important;
14521454
background-repeat: no-repeat;
14531455
margin: 5px;
14541456
text-align: center;
1457+
border-radius: 4px;
14551458
}
14561459

14571460
.lb-video-player {
@@ -2671,3 +2674,12 @@ height: unset !important;
26712674
}
26722675

26732676
}
2677+
2678+
/* Override WordPress theme anchor underline for media thumbnails */
2679+
#opio-review-feed a,
2680+
.outer a,
2681+
#root a,
2682+
#media-container a,
2683+
[data-nitro-exclude] a {
2684+
text-decoration: none !important;
2685+
}

assets/js/opio-main.js

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,51 @@ function opioToggleStuff() {
3232
//custom js functions
3333
function displayLargeImage(imageId, revId) {
3434
var elem = document.querySelector(`#largerevimg-${revId}`);
35-
elem.innerHTML = '<div style="display: inline-block; width: 98.5%; height: 400px; background-position: center center; background-size: cover; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;https://images.files.ca/800x800/'+imageId+'.jpg?nocrop=1&quot;); opacity: 1; transition: opacity 1s ease 0s;"></div><div><div style="position: absolute; z-index: 1; top: 40%; right: 0px; width: 5%; margin: 25px;"></div><div style="display: none;"></div></div>';
35+
if (!elem) return;
36+
var imgUrl = 'https://images.files.ca/800x800/' + imageId + '.jpg?nocrop=1';
37+
elem.innerHTML = '<div style="display: inline-block; width: 98.5%; height: 400px; background-color: #f0f0f0; margin: 5px; text-align: center; display: flex; align-items: center; justify-content: center;">Loading...</div>';
38+
var img = new Image();
39+
img.onload = function() {
40+
var containerWidth = elem.offsetWidth ? elem.offsetWidth * 0.985 : 300;
41+
var aspectRatio = img.naturalHeight / img.naturalWidth;
42+
var calculatedHeight = Math.min(containerWidth * aspectRatio, 400);
43+
var isPortrait = img.naturalHeight > img.naturalWidth;
44+
var bgPosition = isPortrait ? 'left center' : 'center center';
45+
elem.innerHTML = '<div style="display: inline-block; width: 98.5%; height: ' + calculatedHeight + 'px; background-position: ' + bgPosition + '; background-size: contain; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;' + imgUrl + '&quot;); opacity: 1; transition: opacity 1s ease 0s; border-radius: 4px;"></div>';
46+
};
47+
img.onerror = function() {
48+
elem.innerHTML = '<div style="display: inline-block; width: 98.5%; height: 400px; background-position: center center; background-size: contain; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;' + imgUrl + '&quot;); opacity: 1; transition: opacity 1s ease 0s;"></div>';
49+
};
50+
img.src = imgUrl;
51+
}
52+
53+
function displayEmbed(embed, revId) {
54+
try {
55+
if (!embed || typeof embed !== 'object') return;
56+
var elem = document.querySelector(`#largerevimg-${revId}`);
57+
if (!elem) return;
58+
var maxHeight = 400;
59+
var containerWidth = elem.offsetWidth ? elem.offsetWidth * 0.985 : 300;
60+
var platform = (embed.platform || 'youtube').toLowerCase().trim();
61+
if (platform === 'youtube' && embed.videoId && typeof embed.videoId === 'string') {
62+
var videoId = embed.videoId.trim();
63+
if (!videoId) return;
64+
var isShort = embed.embedType === 'short' || (embed.url && typeof embed.url === 'string' && embed.url.indexOf('/shorts/') !== -1);
65+
var iframeHtml;
66+
if (isShort) {
67+
var shortWidth = maxHeight * (9 / 16);
68+
iframeHtml = '<div style="display: inline-block; width: ' + shortWidth + 'px; height: ' + maxHeight + 'px; margin: 5px; position: relative; background: #000; border-radius: 4px; overflow: hidden; vertical-align: top;"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/' + videoId + '?autoplay=1&modestbranding=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="position: absolute; top: 0; left: 0;"></iframe></div>';
69+
} else {
70+
var videoHeight = Math.min(containerWidth * 0.5625, maxHeight);
71+
iframeHtml = '<div style="width: 98.5%; height: ' + videoHeight + 'px; margin: 5px; position: relative; background: #000; border-radius: 4px; overflow: hidden;"><iframe width="100%" height="100%" src="https://www.youtube.com/embed/' + videoId + '?autoplay=1&modestbranding=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="position: absolute; top: 0; left: 0;"></iframe></div>';
72+
}
73+
elem.innerHTML = iframeHtml;
74+
} else if (embed.url && typeof embed.url === 'string' && embed.url.trim()) {
75+
window.open(embed.url.trim(), '_blank');
76+
}
77+
} catch (e) {
78+
console.error('displayEmbed error:', e);
79+
}
3680
}
3781

3882
function writeComment(revId) {
@@ -110,10 +154,30 @@ function shareTwitterUrl(reviewFeedUrl, reviewId) {
110154

111155
function loadMore(business_id) {
112156
var elem = document.querySelector(`#loadMoreOpioDivButton`);
113-
elem.style.backgroundColor='rgb(192, 199, 205)';
157+
// Handle case where event object is passed instead of business_id
158+
if (!business_id || typeof business_id === 'object') {
159+
// Try data attribute first
160+
business_id = elem ? elem.getAttribute('data-entity-id') : null;
161+
// Fallback: try global variable set by feed
162+
if (!business_id && typeof window.opioEntityId !== 'undefined') {
163+
business_id = window.opioEntityId;
164+
}
165+
// Fallback: try to find from feed container
166+
if (!business_id) {
167+
var feedContainer = document.querySelector('[data-opio-entity-id]');
168+
if (feedContainer) {
169+
business_id = feedContainer.getAttribute('data-opio-entity-id');
170+
}
171+
}
172+
}
173+
if (!business_id) {
174+
console.error('loadMore: missing business_id');
175+
return;
176+
}
177+
elem.style.backgroundColor='rgb(192, 199, 205)';
114178
elem.style.cursor = 'not-allowed';
115179
elem.innerHTML='Loading ...';
116-
180+
117181
var body = {};
118182
var xhttp = new XMLHttpRequest();
119183
// window.nextPageToken = LastEvaluatedKey;
@@ -127,7 +191,7 @@ function loadMore(business_id) {
127191
return ;
128192
}
129193
var reviewFeedUrl = newReviews.reviews[0].entityInfo.reviewFeedUrls['5734f48a0b64d7382829fdf7'];
130-
194+
131195
// newReviews = newReviews.reviews.filter((review) => {
132196
// return review.propertyId == '5734f48a0b64d7382829fdf7' && (review.status == 'published' || review.status == 'guest') && review.deleted == false;
133197
// });
@@ -143,7 +207,7 @@ function loadMore(business_id) {
143207
var adminApiKey = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOjE2NDQzNDY2MzEsInVzZXJfaWQiOiJrdHN4dzlramxkZ3RzZDNqMSIsImV4cCI6MTI5NzY0NDM0NjYzMX0.FZeMMsZlix1eQ1aJFmQ0MV_L_ezFb4RhrqCIhceTT-w';
144208
xhttp.setRequestHeader("Content-Type", "application/json");
145209
xhttp.setRequestHeader("authorization", `Bearer ${adminApiKey}`);
146-
xhttp.send(JSON.stringify({LastEvaluatedKey: window.nextPageToken}));`<div id="opio-review-feed" >`
210+
xhttp.send(JSON.stringify({LastEvaluatedKey: window.nextPageToken}));
147211
}
148212

149213
function insertDivs(newReviews, reviewsDiv, loadMoreDiv, reviewFeedUrl, business) {
@@ -352,24 +416,35 @@ function addReview(rev, index, reviewFeedUrl, business) {
352416
reviewBuilder += taggedEmployeesBuilder(taggedEmployees, rev, reviewFontColor);
353417
reviewBuilder += `<span style="font-size: 14px; font-weight: 500; color: ${reviewFontColor}">Employees tagged in this review</span>`;
354418
}
355-
if((rev.images === null || (rev.images && rev.images.length == 0)) && (rev.videos === null || (rev.videos && rev.videos.length == 0))) {
419+
var hasMedia = (rev.images && rev.images.length > 0) || (rev.videos && rev.videos.length > 0) || (rev.embeds && rev.embeds.length > 0);
420+
if(!hasMedia) {
356421
reviewBuilder += `<div id="media-container" style="padding-bottom: 10px;"></div>`;
357422
} else {
358423
reviewBuilder += `<div id="media-container" style="padding-bottom: 10px;">`;
424+
reviewBuilder += `<div id="largerevimg-${rev._id}"></div>`;
359425
if(rev.images) {
360-
reviewBuilder += `<div id="largerevimg-${rev._id}"></div>`;
361426
rev.images.forEach(image => {
362-
reviewBuilder += `<a onclick="displayLargeImage(${image.imageId}, ${rev._id})"><div style="display: inline-block; width: 72px; height: 72px; background-position: center center; background-size: cover; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;https://images.files.ca/200x200/${image.imageId}.jpg?nocrop=1&quot;);">
363-
</div></a>`;
427+
reviewBuilder += `<a onclick="displayLargeImage('${image.imageId}', '${rev._id}')" style="cursor: pointer; text-decoration: none;"><div style="display: inline-block; width: 72px; height: 72px; background-position: center center; background-size: cover; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;https://images.files.ca/200x200/${image.imageId}.jpg?nocrop=1&quot;); border-radius: 4px;"></div></a>`;
364428
});
365429
}
366-
430+
367431
if(rev.videos) {
368432
rev.videos.forEach(video => {
369433
reviewBuilder += `<div><video preload="auto" controls="" style="height: auto; margin: 5px; width: 100%; transition: width 1s ease-out 0s, height 1s ease-out 0s;"><source src="https://videocdn.n49.ca/mp4sdpad480p/${video.videoId}.mp4#t=0.1" type="video/mp4"></video></div>`;
370434
});
371-
}
372-
435+
}
436+
437+
if(rev.embeds) {
438+
rev.embeds.slice(0, 3).forEach(embed => {
439+
var thumbUrl = embed.thumbnailUrl || '';
440+
if(!thumbUrl && embed.platform === 'youtube' && embed.videoId) {
441+
thumbUrl = 'https://img.youtube.com/vi/' + embed.videoId + '/hqdefault.jpg';
442+
}
443+
var embedData = JSON.stringify({platform: embed.platform || 'youtube', videoId: embed.videoId || '', embedType: embed.embedType || 'video', url: embed.url || ''}).replace(/'/g, "\\'");
444+
reviewBuilder += `<a onclick="displayEmbed(JSON.parse('${embedData}'), '${rev._id}')" style="cursor: pointer; text-decoration: none;"><div style="display: inline-block; width: 72px; height: 72px; background-position: center center; background-size: cover; background-repeat: no-repeat; margin: 5px; text-align: center; background-image: url(&quot;${thumbUrl}&quot;); position: relative; border-radius: 4px; background-color: #f0f0f0;"><div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; background: rgba(225,232,237,0.9); border-radius: 50%; display: flex; align-items: center; justify-content: center;"><svg width="12" height="12" viewBox="0 0 24 24" fill="rgb(99,114,130)" style="margin-left: 2px;"><path d="M8 5v14l11-7z"/></svg></div></div></a>`;
445+
});
446+
}
447+
373448
reviewBuilder += '</div>';
374449
}
375450
reviewBuilder += `<div style="display: inline-block; margin-top: 10px; margin-bottom:10px;"></div>`;

0 commit comments

Comments
 (0)