Add bundles tab for package pricing
This commit is contained in:
@@ -1698,6 +1698,7 @@
|
|||||||
let pendingVoteOptionId = null;
|
let pendingVoteOptionId = null;
|
||||||
let pendingVoteRemove = false;
|
let pendingVoteRemove = false;
|
||||||
let pendingStableOptionOrder = null;
|
let pendingStableOptionOrder = null;
|
||||||
|
const BUNDLE_TAB_ID = 'bundles';
|
||||||
|
|
||||||
// ── Init ───────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────
|
||||||
function init() {
|
function init() {
|
||||||
@@ -1721,8 +1722,9 @@
|
|||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
const num = parseInt(e.key);
|
const num = parseInt(e.key);
|
||||||
if (num >= 1 && num <= state.categories.length) {
|
const tabIds = getPrimaryTabIds();
|
||||||
setTab(state.categories[num - 1].id);
|
if (num >= 1 && num <= tabIds.length) {
|
||||||
|
setTab(tabIds[num - 1]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2039,6 +2041,72 @@
|
|||||||
return [typeLabel, basisLabel].filter(Boolean).join(' · ');
|
return [typeLabel, basisLabel].filter(Boolean).join(' · ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTabDefinitions() {
|
||||||
|
const tabs = Array.isArray(state.categories) ? state.categories.map((cat) => ({ ...cat })) : [];
|
||||||
|
const bundleTab = { id: BUNDLE_TAB_ID, name: 'Bundles', emoji: '🧳' };
|
||||||
|
const hotelIndex = tabs.findIndex((cat) => cat.id === 'hotel');
|
||||||
|
if (hotelIndex >= 0) {
|
||||||
|
tabs.splice(hotelIndex + 1, 0, bundleTab);
|
||||||
|
} else {
|
||||||
|
tabs.unshift(bundleTab);
|
||||||
|
}
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimaryTabIds() {
|
||||||
|
return getTabDefinitions().map((tab) => tab.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabMeta(id) {
|
||||||
|
if (id === 'map') return { id: 'map', name: 'Map', emoji: '🗺️' };
|
||||||
|
return getTabDefinitions().find((tab) => tab.id === id) || { id, name: id, emoji: '📁' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBundleSource(source) {
|
||||||
|
return String(source?.bookingType || '').toLowerCase() === 'package';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStandaloneComponentTab(tabId) {
|
||||||
|
return ['hotel', 'flight', 'golf', 'nightlife', 'excursion'].includes(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackageLink(link) {
|
||||||
|
const haystack = `${link?.label || ''} ${link?.url || ''}`.toLowerCase();
|
||||||
|
return /\b(costco|apple vacations|cheapcaribbean|package|bundle|flight[- ]?hotel|hotel[- ]?flight)\b/.test(haystack);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleLinksForTab(opt, tabId = activeTab) {
|
||||||
|
const links = Array.isArray(opt.links) ? opt.links : [];
|
||||||
|
if (tabId === BUNDLE_TAB_ID || !isStandaloneComponentTab(tabId)) {
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
return links.filter((link) => !isPackageLink(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableSourcesForTab(opt, tabId = activeTab) {
|
||||||
|
const sources = getAvailableSources(opt);
|
||||||
|
if (tabId === BUNDLE_TAB_ID) {
|
||||||
|
return sources.filter(isBundleSource);
|
||||||
|
}
|
||||||
|
if (isStandaloneComponentTab(tabId)) {
|
||||||
|
return sources.filter((source) => !isBundleSource(source));
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleOptionsForTab(tabId = activeTab) {
|
||||||
|
const sourcesForTab = (opt) => getAvailableSourcesForTab(opt, tabId).length > 0;
|
||||||
|
if (tabId === BUNDLE_TAB_ID) {
|
||||||
|
return state.options.filter((opt) => opt.approved && sourcesForTab(opt));
|
||||||
|
}
|
||||||
|
return state.options.filter((opt) => opt.categoryId === tabId && opt.approved && sourcesForTab(opt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabOptionCount(tabId) {
|
||||||
|
if (tabId === 'map' || tabId === 'results') return '';
|
||||||
|
return String(getVisibleOptionsForTab(tabId).length);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSourceKey(value) {
|
function normalizeSourceKey(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -2071,8 +2139,11 @@
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptionSelectedSourceKey(opt) {
|
function getOptionSelectedSourceKey(opt, tabId = activeTab) {
|
||||||
const availableSources = getAvailableSources(opt);
|
const availableSources = getAvailableSourcesForTab(opt, tabId);
|
||||||
|
if (!availableSources.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const stored = state.priceSourceSelections?.[opt.id];
|
const stored = state.priceSourceSelections?.[opt.id];
|
||||||
const normalizedStored = stored ? normalizeSourceKey(stored) : '';
|
const normalizedStored = stored ? normalizeSourceKey(stored) : '';
|
||||||
if (normalizedStored && availableSources.some(source => source.sourceKey === normalizedStored)) {
|
if (normalizedStored && availableSources.some(source => source.sourceKey === normalizedStored)) {
|
||||||
@@ -2085,17 +2156,17 @@
|
|||||||
: availableSources[0]?.sourceKey || 'unknown-source';
|
: availableSources[0]?.sourceKey || 'unknown-source';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptionSourceSeries(opt, sourceKey = getOptionSelectedSourceKey(opt)) {
|
function getOptionSourceSeries(opt, sourceKey, tabId = activeTab) {
|
||||||
const normalizedKey = normalizeSourceKey(sourceKey);
|
const normalizedKey = normalizeSourceKey(sourceKey || getOptionSelectedSourceKey(opt, tabId));
|
||||||
if (opt.priceHistoryBySource && Array.isArray(opt.priceHistoryBySource[normalizedKey])) {
|
if (opt.priceHistoryBySource && Array.isArray(opt.priceHistoryBySource[normalizedKey])) {
|
||||||
return opt.priceHistoryBySource[normalizedKey];
|
return opt.priceHistoryBySource[normalizedKey];
|
||||||
}
|
}
|
||||||
return Array.isArray(opt.priceHistory) ? opt.priceHistory : [];
|
return Array.isArray(opt.priceHistory) ? opt.priceHistory : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptionSourceMeta(opt, sourceKey = getOptionSelectedSourceKey(opt)) {
|
function getOptionSourceMeta(opt, sourceKey, tabId = activeTab) {
|
||||||
const normalizedKey = normalizeSourceKey(sourceKey);
|
const normalizedKey = normalizeSourceKey(sourceKey || getOptionSelectedSourceKey(opt, tabId));
|
||||||
return getAvailableSources(opt).find(source => source.sourceKey === normalizedKey) || null;
|
return getAvailableSourcesForTab(opt, tabId).find(source => source.sourceKey === normalizedKey) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOptionSource(optionId, sourceKey) {
|
function setOptionSource(optionId, sourceKey) {
|
||||||
@@ -2129,8 +2200,8 @@
|
|||||||
return new Map(state.options.map((opt, index) => [opt.id, index]));
|
return new Map(state.options.map((opt, index) => [opt.id, index]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedOptionPrice(opt) {
|
function getSelectedOptionPrice(opt, tabId = activeTab) {
|
||||||
const selectedSeries = getOptionSourceSeries(opt);
|
const selectedSeries = getOptionSourceSeries(opt, undefined, tabId);
|
||||||
const selectedPoint = selectedSeries.at(-1) || opt.latestPricePoint || null;
|
const selectedPoint = selectedSeries.at(-1) || opt.latestPricePoint || null;
|
||||||
return typeof selectedPoint?.price === 'number' ? selectedPoint.price : null;
|
return typeof selectedPoint?.price === 'number' ? selectedPoint.price : null;
|
||||||
}
|
}
|
||||||
@@ -2150,8 +2221,8 @@
|
|||||||
return ascending ? aVotes - bVotes : bVotes - aVotes;
|
return ascending ? aVotes - bVotes : bVotes - aVotes;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const aPrice = getSelectedOptionPrice(a);
|
const aPrice = getSelectedOptionPrice(a, activeTab);
|
||||||
const bPrice = getSelectedOptionPrice(b);
|
const bPrice = getSelectedOptionPrice(b, activeTab);
|
||||||
const aHasPrice = typeof aPrice === 'number';
|
const aHasPrice = typeof aPrice === 'number';
|
||||||
const bHasPrice = typeof bPrice === 'number';
|
const bHasPrice = typeof bPrice === 'number';
|
||||||
|
|
||||||
@@ -2243,7 +2314,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSourceSelect(opt) {
|
function renderSourceSelect(opt) {
|
||||||
const sources = getAvailableSources(opt);
|
const sources = getAvailableSourcesForTab(opt);
|
||||||
if (sources.length <= 1) {
|
if (sources.length <= 1) {
|
||||||
const source = sources[0] || null;
|
const source = sources[0] || null;
|
||||||
const sourceLabel = source?.sourceLabel || opt.automationInsights?.source || 'Unknown source';
|
const sourceLabel = source?.sourceLabel || opt.automationInsights?.source || 'Unknown source';
|
||||||
@@ -2274,11 +2345,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderOptionFacts(opt) {
|
function renderOptionFacts(opt) {
|
||||||
const availableSources = getAvailableSources(opt);
|
const tabId = activeTab;
|
||||||
const selectedSourceKey = getOptionSelectedSourceKey(opt);
|
const availableSources = getAvailableSourcesForTab(opt, tabId);
|
||||||
const selectedSeries = getOptionSourceSeries(opt, selectedSourceKey);
|
const selectedSourceKey = getOptionSelectedSourceKey(opt, tabId);
|
||||||
const selectedMeta = getOptionSourceMeta(opt, selectedSourceKey);
|
const selectedSeries = getOptionSourceSeries(opt, selectedSourceKey, tabId);
|
||||||
const selectedPoint = selectedSeries.at(-1) || opt.latestPricePoint || null;
|
const selectedMeta = getOptionSourceMeta(opt, selectedSourceKey, tabId);
|
||||||
|
const selectedPoint = selectedSeries.at(-1) || ((tabId === BUNDLE_TAB_ID || !isStandaloneComponentTab(tabId)) ? opt.latestPricePoint : null);
|
||||||
const insights = selectedPoint ? {
|
const insights = selectedPoint ? {
|
||||||
source: selectedMeta?.sourceLabel || selectedPoint.source || 'Automation feed',
|
source: selectedMeta?.sourceLabel || selectedPoint.source || 'Automation feed',
|
||||||
sourceUrl: selectedMeta?.sourceUrl || selectedPoint.sourceUrl || null,
|
sourceUrl: selectedMeta?.sourceUrl || selectedPoint.sourceUrl || null,
|
||||||
@@ -2288,7 +2360,16 @@
|
|||||||
decisionNote: selectedPoint.decisionNote || null,
|
decisionNote: selectedPoint.decisionNote || null,
|
||||||
displayPrice: selectedPoint.displayPrice || null,
|
displayPrice: selectedPoint.displayPrice || null,
|
||||||
currency: selectedPoint.currency || 'USD',
|
currency: selectedPoint.currency || 'USD',
|
||||||
} : (opt.automationInsights || {});
|
} : {
|
||||||
|
source: selectedMeta?.sourceLabel || opt.automationInsights?.source || 'Automation feed',
|
||||||
|
sourceUrl: selectedMeta?.sourceUrl || opt.automationInsights?.sourceUrl || null,
|
||||||
|
bookingType: selectedMeta?.bookingType || null,
|
||||||
|
priceBasis: selectedMeta?.priceBasis || null,
|
||||||
|
availability: null,
|
||||||
|
decisionNote: null,
|
||||||
|
displayPrice: null,
|
||||||
|
currency: 'USD',
|
||||||
|
};
|
||||||
const currentPrice = typeof selectedPoint?.price === 'number' ? formatCurrency(selectedPoint.price, insights.currency || 'USD') : '';
|
const currentPrice = typeof selectedPoint?.price === 'number' ? formatCurrency(selectedPoint.price, insights.currency || 'USD') : '';
|
||||||
const priceLabel = currentPrice || insights.displayPrice || 'Not yet tracked';
|
const priceLabel = currentPrice || insights.displayPrice || 'Not yet tracked';
|
||||||
const priceContext = getPointContext(selectedPoint);
|
const priceContext = getPointContext(selectedPoint);
|
||||||
@@ -2298,12 +2379,16 @@
|
|||||||
selectedMeta?.priceBasis || insights.priceBasis,
|
selectedMeta?.priceBasis || insights.priceBasis,
|
||||||
);
|
);
|
||||||
const statusLabel = selectedPoint?.availability || selectedPoint?.decisionNote || insights.availability || insights.decisionNote || 'Matched from live search';
|
const statusLabel = selectedPoint?.availability || selectedPoint?.decisionNote || insights.availability || insights.decisionNote || 'Matched from live search';
|
||||||
const overviewItems = normalizeTextList(opt.details);
|
const packageDetailPattern = /\b(package|bundle|bundled|costco|apple|flight[- ]?included|transfer[- ]included|flight\+hotel|hotel\+flight)\b/i;
|
||||||
const autoHighlights = normalizeTextList(selectedPoint?.highlights || opt.automationInsights?.highlights);
|
const filterStandaloneDetails = (items) => (tabId === BUNDLE_TAB_ID || !isStandaloneComponentTab(tabId))
|
||||||
const features = normalizeTextList(selectedPoint?.features || opt.automationInsights?.features);
|
? normalizeTextList(items)
|
||||||
const amenities = normalizeTextList(selectedPoint?.amenities || opt.automationInsights?.amenities);
|
: normalizeTextList(items).filter((item) => !packageDetailPattern.test(item));
|
||||||
const inclusions = normalizeTextList(selectedPoint?.inclusions || opt.automationInsights?.inclusions);
|
const overviewItems = filterStandaloneDetails(opt.details);
|
||||||
const limitations = normalizeTextList(selectedPoint?.limitations || opt.automationInsights?.limitations);
|
const autoHighlights = filterStandaloneDetails(selectedPoint?.highlights || opt.automationInsights?.highlights);
|
||||||
|
const features = filterStandaloneDetails(selectedPoint?.features || opt.automationInsights?.features);
|
||||||
|
const amenities = filterStandaloneDetails(selectedPoint?.amenities || opt.automationInsights?.amenities);
|
||||||
|
const inclusions = filterStandaloneDetails(selectedPoint?.inclusions || opt.automationInsights?.inclusions);
|
||||||
|
const limitations = filterStandaloneDetails(selectedPoint?.limitations || opt.automationInsights?.limitations);
|
||||||
const includedComponents = normalizeTextList(selectedPoint?.includedComponents || opt.automationInsights?.includedComponents);
|
const includedComponents = normalizeTextList(selectedPoint?.includedComponents || opt.automationInsights?.includedComponents);
|
||||||
const flightsIncluded = includedComponents.some(item => /flight/i.test(item));
|
const flightsIncluded = includedComponents.some(item => /flight/i.test(item));
|
||||||
const sourceMetaLine = selectedMeta
|
const sourceMetaLine = selectedMeta
|
||||||
@@ -2380,7 +2465,7 @@
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt.categoryId === 'hotel' && flightsIncluded) {
|
if (tabId === BUNDLE_TAB_ID && flightsIncluded) {
|
||||||
sections.push(`
|
sections.push(`
|
||||||
<div class="option-detail-section">
|
<div class="option-detail-section">
|
||||||
<div class="option-detail-title">Flights included</div>
|
<div class="option-detail-title">Flights included</div>
|
||||||
@@ -2404,10 +2489,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPriceTrend(opt) {
|
function renderPriceTrend(opt) {
|
||||||
const selectedSourceKey = getOptionSelectedSourceKey(opt);
|
const tabId = activeTab;
|
||||||
const series = getOptionSourceSeries(opt, selectedSourceKey)
|
const selectedSourceKey = getOptionSelectedSourceKey(opt, tabId);
|
||||||
|
const series = getOptionSourceSeries(opt, selectedSourceKey, tabId)
|
||||||
.filter(point => typeof point.price === 'number' && !Number.isNaN(point.price));
|
.filter(point => typeof point.price === 'number' && !Number.isNaN(point.price));
|
||||||
const selectedMeta = getOptionSourceMeta(opt, selectedSourceKey);
|
const selectedMeta = getOptionSourceMeta(opt, selectedSourceKey, tabId);
|
||||||
|
|
||||||
if (series.length === 0) {
|
if (series.length === 0) {
|
||||||
return `
|
return `
|
||||||
@@ -2591,7 +2677,7 @@
|
|||||||
// ── Tabs ───────────────────────────────────────────────────
|
// ── Tabs ───────────────────────────────────────────────────
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
const bar = document.getElementById('tabsBar');
|
const bar = document.getElementById('tabsBar');
|
||||||
const catsWithMap = [...state.categories, { id: 'map', name: 'Map', emoji: '🗺️' }];
|
const catsWithMap = [...getTabDefinitions(), { id: 'map', name: 'Map', emoji: '🗺️' }];
|
||||||
bar.innerHTML = catsWithMap.map(cat => `
|
bar.innerHTML = catsWithMap.map(cat => `
|
||||||
<div class="tab${cat.id === activeTab ? ' active' : ''}"
|
<div class="tab${cat.id === activeTab ? ' active' : ''}"
|
||||||
role="tab"
|
role="tab"
|
||||||
@@ -2603,15 +2689,15 @@
|
|||||||
onkeydown="handleTabKey(event, '${cat.id}')">
|
onkeydown="handleTabKey(event, '${cat.id}')">
|
||||||
<span class="tab-emoji">${cat.emoji}</span>
|
<span class="tab-emoji">${cat.emoji}</span>
|
||||||
${cat.name}
|
${cat.name}
|
||||||
<span class="tab-count" id="tab-count-${cat.id}">${cat.id === 'results' ? '' : cat.id === 'map' ? '' : state.options.filter(o => o.categoryId === cat.id).length}</span>
|
<span class="tab-count" id="tab-count-${cat.id}">${getTabOptionCount(cat.id)}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
bar.setAttribute('role', 'tablist');
|
bar.setAttribute('role', 'tablist');
|
||||||
bar.setAttribute('aria-label', 'Voting categories');
|
bar.setAttribute('aria-label', 'Trip tabs');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTabKey(event, catId) {
|
function handleTabKey(event, catId) {
|
||||||
const cats = [...state.categories.map(c => c.id), 'map'];
|
const cats = [...getPrimaryTabIds(), 'map'];
|
||||||
const idx = cats.indexOf(catId);
|
const idx = cats.indexOf(catId);
|
||||||
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -2644,7 +2730,7 @@
|
|||||||
optionsList.style.display = 'block';
|
optionsList.style.display = 'block';
|
||||||
if (addSection) addSection.style.display = '';
|
if (addSection) addSection.style.display = '';
|
||||||
}
|
}
|
||||||
document.getElementById('optionsList')?.setAttribute('aria-label', state.categories.find(c => c.id === id)?.name + ' options');
|
document.getElementById('optionsList')?.setAttribute('aria-label', `${getTabMeta(id)?.name || id} options`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render options ────────────────────────────────────────
|
// ── Render options ────────────────────────────────────────
|
||||||
@@ -2713,13 +2799,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Regular voting tabs ─────────────────────────────────
|
// ── Regular voting tabs ─────────────────────────────────
|
||||||
const opts = state.options.filter(o => o.categoryId === activeTab && o.approved);
|
const opts = getVisibleOptionsForTab(activeTab);
|
||||||
|
|
||||||
document.getElementById('totalVotersCount').textContent =
|
document.getElementById('totalVotersCount').textContent =
|
||||||
state.totalVoters ? `👥 ${state.totalVoters} voter${state.totalVoters !== 1 ? 's' : ''}` : '';
|
state.totalVoters ? `👥 ${state.totalVoters} voter${state.totalVoters !== 1 ? 's' : ''}` : '';
|
||||||
|
|
||||||
if (opts.length === 0) {
|
if (opts.length === 0) {
|
||||||
list.innerHTML = `<div class="empty-state"><div class="empty-emoji">🗳️</div>No options yet. Be the first to add one below!</div>`;
|
const emptyCopy = activeTab === BUNDLE_TAB_ID
|
||||||
|
? 'No bundle options yet. Check back after the next live pricing run.'
|
||||||
|
: 'No options yet. Be the first to add one below!';
|
||||||
|
list.innerHTML = `<div class="empty-state"><div class="empty-emoji">🗳️</div>${emptyCopy}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2733,11 +2822,14 @@
|
|||||||
const votePct = maxVotes > 0 ? (voteEntries.length / maxVotes * 100) : 0;
|
const votePct = maxVotes > 0 ? (voteEntries.length / maxVotes * 100) : 0;
|
||||||
const hasVoted = state.voterName && voteEntries.some(v => v.name === state.voterName);
|
const hasVoted = state.voterName && voteEntries.some(v => v.name === state.voterName);
|
||||||
const voteList = voteEntries.map(v => v.name).join(', ');
|
const voteList = voteEntries.map(v => v.name).join(', ');
|
||||||
const linkPills = opt.links && opt.links.length
|
const visibleLinks = getVisibleLinksForTab(opt, activeTab);
|
||||||
? `<div class="option-links">${opt.links.map(link => `
|
const linkPills = visibleLinks.length
|
||||||
|
? `<div class="option-links">${visibleLinks.map(link => `
|
||||||
<a href="${link.url}" target="_blank" rel="noopener noreferrer" class="option-pill" onclick="event.stopPropagation()">${link.label}</a>
|
<a href="${link.url}" target="_blank" rel="noopener noreferrer" class="option-pill" onclick="event.stopPropagation()">${link.label}</a>
|
||||||
`).join('')}</div>`
|
`).join('')}</div>`
|
||||||
: (opt.url ? `<a href="${opt.url}" target="_blank" rel="noopener noreferrer" class="option-link" onclick="event.stopPropagation()">🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}</a>` : '');
|
: (opt.url && !isPackageLink({ label: opt.name, url: opt.url })
|
||||||
|
? `<a href="${opt.url}" target="_blank" rel="noopener noreferrer" class="option-link" onclick="event.stopPropagation()">🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}</a>`
|
||||||
|
: '');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="option-card${hasVoted ? ' voted' : ''}" data-option-id="${escapeHtml(opt.id)}">
|
<div class="option-card${hasVoted ? ' voted' : ''}" data-option-id="${escapeHtml(opt.id)}">
|
||||||
|
|||||||
Reference in New Issue
Block a user