Vind accommodatie in geselecteerde bestemmingen, zoek vluchten voor reizen dichtbij en ver weg, en boek een huurauto die u verder vrijheid geeft. Alles verzameld in een overzichtelijke en gebruiksvriendelijke oplossing.
BNO Travel verzamelt belangrijke onderdelen van de reis in één oplossing – afgestemd op zowel de behoeften van vandaag als verdere groei.
Vind uw volgende bestemming
Van Noorse bergen en de Alpen tot zon, steden en verre reizen. Ontdek geselecteerde bestemmingen, vind accommodatie en plan uw reis verder met vluchten en een huurauto.
Bestemmingen waar BNO Travel een bijzonder sterk aanbod aan accommodaties heeft – van Scandinavië en de Alpen tot het Middellandse Zeegebied en Canada.
Uitgelicht
Trysil
Skiën, bergen, gezinsactiviteiten en belevenissen het hele jaar door in een van de populairste bergbestemmingen van Noorwegen.
Ontdek zorgvuldig geselecteerde verblijven in populaire gebieden en reisbestemmingen, met oplossingen die passen bij zowel korte reizen als langere verblijven.
Een overzichtelijkere en flexibelere manier om de reis te plannen
BNO Travel is gebouwd om reisplanning eenvoudiger te maken. Met duidelijke ingangen naar accommodatie, vluchten en huurauto krijgt u een overzichtelijkere ervaring – zowel wanneer u vandaag iets concreets wilt boeken als wanneer u mogelijkheden voor de volgende reis wilt verkennen.
BNO Travel-app – Houd beter overzicht over uw reis, ook onderweg
BNO Travel is ontwikkeld om goed te werken op zowel de website als in de app. Start de planning waar het u het beste uitkomt, en gebruik dezelfde oplossing om overzicht te houden over de verdere reis.
Plan eenvoudig op mobiel en desktop
Krijg een meer samenhangende reiservaring
Volg uw reis in een oplossing die is gemaakt om mee te groeien met uw behoeften
Heeft u een hut, appartement of andere accommodatie in een aantrekkelijk gebied? BNO Travel geeft eigenaren en verhuurders de mogelijkheid om deel te worden van een professioneel reisplatform met focus op presentatie, zichtbaarheid en een overzichtelijke ervaring voor gasten.
Wij maken het mogelijk dat accommodaties in geselecteerde bestemmingen op een betrouwbare en commerciële manier worden gepresenteerd, als onderdeel van een complete reiservaring. Het doel is om het eenvoudiger te maken relevante gasten te bereiken en uw aanbod in een sterkere digitale context te presenteren.
';
});
}
appendRichHtml(html);
}
function normalizeIntentText(value) {
return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/å/g, 'a').replace(/æ/g, 'ae').replace(/ø/g, 'o');
}
function classifyWebTravelIntent(message) {
const value = normalizeIntentText(message);
const categories = [
/overnatting|hytte|leilighet|hotell|hotel|accommodation|lodging/,
/fly|flight|flybillett|avgang/,
/aktivitet|restaurant|reiseforslag|reiseplan|skiferie|ferie/
].filter(function (pattern) { return pattern.test(value); }).length;
if (/reiseforslag|reiseplan|skiferie|familieferie|sett opp|lag et forslag/.test(value) || categories >= 2) return 'trip_planning';
if (/fly|flight|flybillett|avgang/.test(value)) return 'flights';
if (/overnatting|hytte|leilighet|hotell|hotel|accommodation|lodging/.test(value)) return 'accommodation';
if (/restaurant|middag|lunsj|mat/.test(value)) return 'restaurants';
if (/aktivitet|opplevelse|ski|museum/.test(value)) return 'activities';
return 'general';
}
function extractWebDestinationHint(message) {
const value = normalizeIntentText(message);
const matches = [
['portes-du-soleil', ['portes du soleil','morzine','les gets','avoriaz','chatel']],
['tignes-val-disere', ['tignes','val d isere','val disere']],
['chamonix', ['chamonix']], ['verbier', ['verbier']], ['trysil', ['trysil']],
['salen', ['salen','saelen']], ['stranda', ['stranda','strandafjellet']],
['grand-massif', ['grand massif','les carroz']], ['pays-du-mont-blanc', ['pays du mont blanc']],
['3-vallees', ['3 vallees','trois vallees','three valleys']], ['mallorca', ['mallorca','palma']],
['costa-brava', ['costa brava']], ['kaprun', ['kaprun']], ['zell-am-see', ['zell am see']],
['vemdalen', ['vemdalen']]
];
for (let i = 0; i < matches.length; i += 1) {
if (matches[i][1].some(function (term) { return value.indexOf(term) >= 0; })) return matches[i][0];
}
return null;
}
function showInitialHint() {
if (chatEl.children.length) return;
widget.classList.add('is-start');
chatEl.innerHTML =
'
' +
'Probeer de BNO AI-reisassistent ' +
'Kies hieronder een suggestie of typ uw vraag in het veld onderaan.' +
'
';
quickPromptsEl.style.display = '';
}
function clearInitialHint() {
widget.classList.remove('is-start');
const hint = chatEl.querySelector('.bno-helper-empty-state');
if (hint) hint.remove();
}
function rememberDestinationFromResponse(data) {
const candidates = [
data && data.tripProposal && data.tripProposal.destination && data.tripProposal.destination.destinationSlug,
data && data.tripProposal && data.tripProposal.destination && data.tripProposal.destination.area,
data && data.searchContext && data.searchContext.params && data.searchContext.params.area,
data && data.detectedDestination
];
for (const candidate of candidates) {
const value = String(candidate || '').trim();
if (value) {
lastDestinationSlug = value.slice(0, 120);
return;
}
}
}
function removeContactForm() {
const card = chatEl.querySelector('[data-bno-contact-card]');
if (card) card.remove();
contactFormOpen = false;
}
function setSupportUiActive() {
humanRowEl.classList.add('is-active');
humanTitleEl.textContent = 'U chat met een van onze medewerkers';
humanHintEl.textContent = 'Directe klantenchat met BNO Travel';
humanButtonEl.classList.add('is-active');
humanButtonEl.innerHTML = '✓U chat met BNO Travel';
humanButtonEl.disabled = true;
quickPromptsEl.style.display = 'none';
queryEl.placeholder = 'Schrijf een bericht aan BNO Travel …';
endSupportBtn.hidden = false;
}
function appendSupportIntro() {
clearInitialHint();
chatEl.insertAdjacentHTML('beforeend',
'
BNO Travel
U chat nu met BNO Travel
Schrijf waarmee u hulp nodig hebt. Zodra een servicemedewerker antwoordt, verschijnt het antwoord hier.
Als u de pagina verlaat voordat we kunnen antwoorden, kunnen we contact met u opnemen via het opgegeven e-mailadres.
'
);
scrollBottom();
}
function showSupportContactForm() {
if (supportToken) {
enterSupportMode();
return;
}
if (contactFormOpen) {
const existingName = chatEl.querySelector('[data-bno-contact-name]');
if (existingName) existingName.focus();
return;
}
clearInitialHint();
contactFormOpen = true;
quickPromptsEl.style.display = 'none';
setMainInputEnabled(false);
setStatus('Vul uw naam en e-mailadres in om een chat met BNO Travel te starten.');
chatEl.insertAdjacentHTML('beforeend',
'
' +
'
' +
'Chat met een van onze medewerkers' +
'
Vul uw naam en e-mailadres in. Zo kunnen we contact met u opnemen als u de website verlaat voordat we kunnen antwoorden.
' +
'' +
'
' +
'
'
);
scrollBottom();
setTimeout(function () {
const nameInput = chatEl.querySelector('[data-bno-contact-name]');
const emailInput = chatEl.querySelector('[data-bno-contact-email]');
if (nameInput && !nameInput.value) nameInput.focus();
else if (emailInput) emailInput.focus();
}, 50);
}
function cancelSupportContactForm() {
removeContactForm();
if (!supportMode) quickPromptsEl.style.display = '';
setMainInputEnabled(true);
setStatus('');
if (!chatEl.children.length) showInitialHint();
queryEl.focus();
}
function enterSupportMode() {
removeContactForm();
if (!supportMode) appendSupportIntro();
supportMode = true;
setSupportUiActive();
setMainInputEnabled(true);
setStatus('Directe klantenchat met BNO Travel', 'success');
if (supportToken) startSupportPolling();
queryEl.focus();
}
function submitSupportContactForm(form) {
const nameInput = form.querySelector('[data-bno-contact-name]');
const emailInput = form.querySelector('[data-bno-contact-email]');
const errorEl = form.querySelector('[data-bno-contact-error]');
const name = String(nameInput && nameInput.value || '').trim();
const email = String(emailInput && emailInput.value || '').trim().toLowerCase();
if (!name) {
errorEl.textContent = 'Vul uw naam in.';
if (nameInput) nameInput.focus();
return;
}
if (!isValidEmail(email)) {
errorEl.textContent = 'Vul een geldig e-mailadres in.';
if (emailInput) emailInput.focus();
return;
}
errorEl.textContent = '';
saveSupportProfile({ name: name, email: email });
enterSupportMode();
}
function resetSupportUi() {
supportMode = false;
contactFormOpen = false;
supportToken = '';
supportProfile = { name: '', email: '' };
sessionStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem(PROFILE_KEY);
seenSupportMessageIds.clear();
if (supportPollTimer) clearInterval(supportPollTimer);
supportPollTimer = null;
setSupportUiIdle();
setMainInputEnabled(true);
setStatus('');
}
function resetWholeConversation() {
resetSupportUi();
travelHistory = [];
lastDestinationSlug = '';
lastPackageOptions = [];
lastPackageVisibleCount = 0;
chatEl.innerHTML = '';
queryEl.value = '';
resizeInput();
showInitialHint();
scrollBottom();
}
async function endSupportConversation() {
const tokenToClose = supportToken;
endSupportBtn.disabled = true;
setStatus('Chat wordt beëindigd …');
try {
if (tokenToClose) {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(tokenToClose), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({ status: 'closed' })
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) {
throw new Error(data && data.error || 'support_close_failed');
}
}
resetWholeConversation();
setStatus('De chat is beëindigd. U kunt een nieuw gesprek starten.', 'success');
} catch (error) {
console.error('[BNO SUPPORT CLOSE]', error);
setStatus('De chat kan op dit moment niet worden beëindigd. Probeer het opnieuw.', 'error');
} finally {
endSupportBtn.disabled = false;
}
}
async function sendTravelHelper(message) {
clearInitialHint();
quickPromptsEl.style.display = 'none';
appendUserMessage(message);
setStatus('De BNO AI-reisassistent denkt na …');
submitEl.disabled = true;
const language = getLanguage();
const nextHistory = travelHistory.concat([{ role: 'user', text: message }]).slice(-16);
try {
const response = await fetch(API_BASE + TRAVEL_HELPER_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
message: message,
lang: language,
replyLanguage: language,
detectedIntent: classifyWebTravelIntent(message),
detectedDestination: extractWebDestinationHint(message),
history: nextHistory,
helperContext: {
replyLanguage: language,
detectedIntent: classifyWebTravelIntent(message),
detectedDestination: extractWebDestinationHint(message),
isTripPlanning: classifyWebTravelIntent(message) === 'trip_planning',
source: 'bno-travel-web',
pageUrl: window.location.href
}
})
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.reply) {
throw new Error(data && data.error || 'travel_helper_failed');
}
rememberDestinationFromResponse(data);
const reply = String(data.reply);
const packageOptions = data.packageAction && Array.isArray(data.packageAction.options) ? data.packageAction.options : [];
travelHistory = nextHistory.concat([{ role: 'assistant', text: reply }]).slice(-16);
if (packageOptions.length) {
lastPackageOptions = packageOptions.slice();
lastPackageVisibleCount = 0;
const narrativeHtml = buildPackageNarrativeHtml(packageOptions);
if (narrativeHtml) appendAssistantRichMessage(narrativeHtml, 'BNO AI-reisassistent');
else appendAssistantMessage(reply, 'BNO AI-reisassistent');
} else {
appendAssistantMessage(reply, 'BNO AI-reisassistent');
}
await renderTravelResponseExtras(data);
setStatus('');
} catch (error) {
console.error('[BNO WEB HELPER]', error);
appendAssistantMessage('Sorry, de BNO AI-reisassistent is momenteel niet beschikbaar. U kunt nog steeds ‘Chat met een van onze medewerkers’ kiezen om contact op te nemen met BNO Travel.', 'BNO AI-reisassistent');
setStatus('Er kon geen antwoord van BNO Reisassistent worden opgehaald.', 'error');
} finally {
submitEl.disabled = false;
}
}
async function createSupportConversation(message) {
if (!supportProfile.name || !isValidEmail(supportProfile.email)) {
throw new Error('support_profile_required');
}
const language = getLanguage();
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
customerName: supportProfile.name,
customerEmail: supportProfile.email,
language: language,
destinationSlug: lastDestinationSlug || null,
pageUrl: window.location.href,
initialMessage: message,
travelHelperContext: {
history: travelHistory.slice(-12),
source: 'bno-travel-web',
pageUrl: window.location.href
}
})
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok || !data.data || !data.data.publicToken) {
throw new Error(data && data.error || 'support_create_failed');
}
supportToken = String(data.data.publicToken);
sessionStorage.setItem(STORAGE_KEY, supportToken);
startSupportPolling();
}
async function sendSupportMessage(message) {
clearInitialHint();
appendUserMessage(message);
setStatus('Wordt verzonden naar BNO Travel …');
submitEl.disabled = true;
try {
if (!supportToken) {
await createSupportConversation(message);
} else {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(supportToken) + '/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({ body: message })
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) throw new Error(data && data.error || 'support_message_failed');
}
setStatus('Uw bericht is verzonden naar BNO Travel.', 'success');
await pollSupportMessages();
} catch (error) {
console.error('[BNO SUPPORT CHAT]', error);
if (String(error && error.message || '') === 'support_profile_required') {
supportMode = false;
setSupportUiIdle();
showSupportContactForm();
setStatus('Naam en e-mailadres zijn vereist voordat het bericht kan worden verzonden.', 'error');
} else {
setStatus('Het bericht kon niet worden verzonden. Probeer het opnieuw.', 'error');
}
} finally {
if (!contactFormOpen) submitEl.disabled = false;
}
}
async function pollSupportMessages() {
if (!supportToken) return;
try {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(supportToken) + '/messages', {
cache: 'no-store'
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) return;
if (data.data && data.data.conversation && data.data.conversation.status === 'closed') {
resetWholeConversation();
setStatus('Deze klantenchat is beëindigd. U kunt een nieuw gesprek starten.', 'success');
return;
}
const messages = Array.isArray(data.data && data.data.messages) ? data.data.messages : [];
const staffMessages = messages.filter(function (message) { return message.sender_type === 'staff'; });
staffMessages.forEach(function (message) {
const messageId = String(message.id || '');
if (!messageId || seenSupportMessageIds.has(messageId)) return;
const staffName = firstText(message.sender_name, message.staff_name, message.display_name);
appendAssistantMessage(String(message.body || ''), staffName ? staffName + ', BNO Travel' : 'BNO Travel');
seenSupportMessageIds.add(messageId);
});
} catch (error) {
console.warn('[BNO SUPPORT POLL]', error);
}
}
function startSupportPolling() {
if (supportPollTimer) clearInterval(supportPollTimer);
pollSupportMessages();
supportPollTimer = window.setInterval(pollSupportMessages, 8000);
}
async function handleSend(messageOverride) {
if (contactFormOpen) return;
const message = String(messageOverride || queryEl.value || '').trim();
if (!message) {
setStatus('Schrijf eerst een bericht.');
return;
}
queryEl.value = '';
resizeInput();
if (!supportMode) {
const selectedPackageIndex = extractPackageChoiceIndex(message);
if (selectedPackageIndex) {
appendUserMessage(message);
openPackageOption(selectedPackageIndex);
return;
}
if (isMorePackageRequest(message)) {
appendUserMessage(message);
await showNextPackageOptions();
return;
}
}
if (supportMode) await sendSupportMessage(message);
else await sendTravelHelper(message);
}
launcher.addEventListener('click', openHelper);
closeBtn.addEventListener('click', closeHelper);
overlay.addEventListener('click', closeHelper);
humanButtonEl.addEventListener('click', function () { showSupportContactForm(); });
endSupportBtn.addEventListener('click', function () { endSupportConversation(); });
submitEl.addEventListener('click', function () { handleSend(); });
queryEl.addEventListener('input', resizeInput);
queryEl.addEventListener('keydown', function (event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSend();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && widget.classList.contains('is-open')) closeHelper();
});
quickPromptsEl.addEventListener('click', function (event) {
const button = event.target.closest('button[data-prompt]');
if (!button || supportMode) return;
handleSend(button.getAttribute('data-prompt') || '');
});
chatEl.addEventListener('click', function (event) {
const packageButton = event.target.closest('[data-bno-package-option-index]');
if (packageButton && !supportMode) {
event.preventDefault();
const index = Number(packageButton.getAttribute('data-bno-package-option-index'));
openPackageOption(index);
return;
}
const moreButton = event.target.closest('[data-bno-show-more-package]');
if (moreButton && !supportMode) {
event.preventDefault();
moreButton.disabled = true;
showNextPackageOptions();
return;
}
const cancel = event.target.closest('[data-bno-contact-cancel]');
if (cancel) {
event.preventDefault();
cancelSupportContactForm();
}
});
chatEl.addEventListener('submit', function (event) {
const form = event.target.closest('[data-bno-contact-form]');
if (!form) return;
event.preventDefault();
submitSupportContactForm(form);
});
showInitialHint();
// Give the closed BNO Reisehjelper launcher one subtle attention cue on page load.
window.setTimeout(startLauncherAttention, 900);
if (supportToken) {
supportMode = true;
clearInitialHint();
appendSupportIntro();
setSupportUiActive();
setStatus('Directe klantenchat met BNO Travel', 'success');
startSupportPolling();
}
})();